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
\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 = 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 session.add(player)
\r
131 def get_or_create_server(session=None, name=None, hashkey=None):
\r
133 Find a server by name or create one if not found. Parameters:
\r
135 session - SQLAlchemy database session factory
\r
136 name - server name of the server to be found or created
\r
137 hashkey - server hashkey
\r
140 # find one by that name, if it exists
\r
141 server = session.query(Server).filter_by(name=name).one()
\r
143 # store new hashkey
\r
144 if server.hashkey != hashkey:
\r
145 server.hashkey = hashkey
\r
146 session.add(server)
\r
148 log.debug("Found existing server {0}".format(server.server_id))
\r
150 except MultipleResultsFound, e:
\r
151 # multiple found, so also filter by hashkey
\r
152 server = session.query(Server).filter_by(name=name).\
\r
153 filter_by(hashkey=hashkey).one()
\r
154 log.debug("Found existing server {0}".format(server.server_id))
\r
156 except NoResultFound, e:
\r
157 # not found, create one
\r
158 server = Server(name=name, hashkey=hashkey)
\r
159 session.add(server)
\r
161 log.debug("Created server {0} with hashkey {1}".format(
\r
162 server.server_id, server.hashkey))
\r
167 def get_or_create_map(session=None, name=None):
\r
169 Find a map by name or create one if not found. Parameters:
\r
171 session - SQLAlchemy database session factory
\r
172 name - map name of the map to be found or created
\r
175 # find one by the name, if it exists
\r
176 gmap = session.query(Map).filter_by(name=name).one()
\r
177 log.debug("Found map id {0}: {1}".format(gmap.map_id,
\r
179 except NoResultFound, e:
\r
180 gmap = Map(name=name)
\r
183 log.debug("Created map id {0}: {1}".format(gmap.map_id,
\r
185 except MultipleResultsFound, e:
\r
186 # multiple found, so use the first one but warn
\r
188 gmaps = session.query(Map).filter_by(name=name).order_by(
\r
191 log.debug("Found map id {0}: {1} but found \
\r
192 multiple".format(gmap.map_id, gmap.name))
\r
197 def create_game(session=None, start_dt=None, game_type_cd=None,
\r
198 server_id=None, map_id=None, winner=None):
\r
200 Creates a game. Parameters:
\r
202 session - SQLAlchemy database session factory
\r
203 start_dt - when the game started (datetime object)
\r
204 game_type_cd - the game type of the game being played
\r
205 server_id - server identifier of the server hosting the game
\r
206 map_id - map on which the game was played
\r
207 winner - the team id of the team that won
\r
209 seq = Sequence('games_game_id_seq')
\r
210 game_id = session.execute(seq)
\r
211 game = Game(game_id=game_id, start_dt=start_dt, game_type_cd=game_type_cd,
\r
212 server_id=server_id, map_id=map_id, winner=winner)
\r
214 log.debug("Created game id {0} on server {1}, map {2} at \
\r
215 {3}".format(game.game_id,
\r
216 server_id, map_id, start_dt))
\r
221 def get_or_create_player(session=None, hashkey=None, nick=None):
\r
223 Finds a player by hashkey or creates a new one (along with a
\r
224 corresponding hashkey entry. Parameters:
\r
226 session - SQLAlchemy database session factory
\r
227 hashkey - hashkey of the player to be found or created
\r
228 nick - nick of the player (in case of a first time create)
\r
231 if re.search('^bot#\d+$', hashkey):
\r
232 player = session.query(Player).filter_by(player_id=1).one()
\r
233 # if we have an untracked player
\r
234 elif re.search('^player#\d+$', hashkey):
\r
235 player = session.query(Player).filter_by(player_id=2).one()
\r
236 # else it is a tracked player
\r
238 # see if the player is already in the database
\r
239 # if not, create one and the hashkey along with it
\r
241 hashkey = session.query(Hashkey).filter_by(
\r
242 hashkey=hashkey).one()
\r
243 player = session.query(Player).filter_by(
\r
244 player_id=hashkey.player_id).one()
\r
245 log.debug("Found existing player {0} with hashkey {1}".format(
\r
246 player.player_id, hashkey.hashkey))
\r
249 session.add(player)
\r
252 # if nick is given to us, use it. If not, use "Anonymous Player"
\r
253 # with a suffix added for uniqueness.
\r
255 player.nick = nick[:128]
\r
257 player.nick = "Anonymous Player #{0}".format(player.player_id)
\r
259 hashkey = Hashkey(player_id=player.player_id, hashkey=hashkey)
\r
260 session.add(hashkey)
\r
261 log.debug("Created player {0} ({2}) with hashkey {1}".format(
\r
262 player.player_id, hashkey.hashkey, player.nick.encode('utf-8')))
\r
266 def create_player_game_stat(session=None, player=None,
\r
267 game=None, player_events=None):
\r
269 Creates game statistics for a given player in a given game. Parameters:
\r
271 session - SQLAlchemy session factory
\r
272 player - Player record of the player who owns the stats
\r
273 game - Game record for the game to which the stats pertain
\r
274 player_events - dictionary for the actual stats that need to be transformed
\r
277 # in here setup default values (e.g. if game type is CTF then
\r
278 # set kills=0, score=0, captures=0, pickups=0, fckills=0, etc
\r
279 # TODO: use game's create date here instead of now()
\r
280 seq = Sequence('player_game_stats_player_game_stat_id_seq')
\r
281 pgstat_id = session.execute(seq)
\r
282 pgstat = PlayerGameStat(player_game_stat_id=pgstat_id,
\r
283 create_dt=datetime.datetime.utcnow())
\r
285 # set player id from player record
\r
286 pgstat.player_id = player.player_id
\r
288 #set game id from game record
\r
289 pgstat.game_id = game.game_id
\r
291 # all games have a score
\r
294 if game.game_type_cd == 'dm':
\r
297 pgstat.suicides = 0
\r
298 elif game.game_type_cd == 'ctf':
\r
300 pgstat.captures = 0
\r
304 pgstat.carrier_frags = 0
\r
306 for (key,value) in player_events.items():
\r
307 if key == 'n': pgstat.nick = value[:128]
\r
308 if key == 't': pgstat.team = value
\r
309 if key == 'rank': pgstat.rank = value
\r
310 if key == 'alivetime':
\r
311 pgstat.alivetime = datetime.timedelta(seconds=int(round(float(value))))
\r
312 if key == 'scoreboard-drops': pgstat.drops = value
\r
313 if key == 'scoreboard-returns': pgstat.returns = value
\r
314 if key == 'scoreboard-fckills': pgstat.carrier_frags = value
\r
315 if key == 'scoreboard-pickups': pgstat.pickups = value
\r
316 if key == 'scoreboard-caps': pgstat.captures = value
\r
317 if key == 'scoreboard-score': pgstat.score = value
\r
318 if key == 'scoreboard-deaths': pgstat.deaths = value
\r
319 if key == 'scoreboard-kills': pgstat.kills = value
\r
320 if key == 'scoreboard-suicides': pgstat.suicides = value
\r
322 # check to see if we had a name, and if
\r
323 # not use the name from the player id
\r
324 if pgstat.nick == None:
\r
325 pgstat.nick = player.nick
\r
327 # if the nick we end up with is different from the one in the
\r
328 # player record, change the nick to reflect the new value
\r
329 if pgstat.nick != player.nick and player.player_id > 2:
\r
330 register_new_nick(session, player, pgstat.nick)
\r
332 # if the player is ranked #1 and it is a team game, set the game's winner
\r
333 # to be the team of that player
\r
334 # FIXME: this is a hack, should be using the 'W' field (not present)
\r
335 if pgstat.rank == '1' and pgstat.team:
\r
336 game.winner = pgstat.team
\r
339 session.add(pgstat)
\r
344 def create_player_weapon_stats(session=None, player=None,
\r
345 game=None, pgstat=None, player_events=None):
\r
347 Creates accuracy records for each weapon used by a given player in a
\r
348 given game. Parameters:
\r
350 session - SQLAlchemy session factory object
\r
351 player - Player record who owns the weapon stats
\r
352 game - Game record in which the stats were created
\r
353 pgstat - Corresponding PlayerGameStat record for these weapon stats
\r
354 player_events - dictionary containing the raw weapon values that need to be
\r
359 for (key,value) in player_events.items():
\r
360 matched = re.search("acc-(.*?)-cnt-fired", key)
\r
362 weapon_cd = matched.group(1)
\r
363 seq = Sequence('player_weapon_stats_player_weapon_stats_id_seq')
\r
364 pwstat_id = session.execute(seq)
\r
365 pwstat = PlayerWeaponStat()
\r
366 pwstat.player_weapon_stats_id = pwstat_id
\r
367 pwstat.player_id = player.player_id
\r
368 pwstat.game_id = game.game_id
\r
369 pwstat.player_game_stat_id = pgstat.player_game_stat_id
\r
370 pwstat.weapon_cd = weapon_cd
\r
372 if 'n' in player_events:
\r
373 pwstat.nick = player_events['n']
\r
375 pwstat.nick = player_events['P']
\r
377 if 'acc-' + weapon_cd + '-cnt-fired' in player_events:
\r
378 pwstat.fired = int(round(float(
\r
379 player_events['acc-' + weapon_cd + '-cnt-fired'])))
\r
380 if 'acc-' + weapon_cd + '-fired' in player_events:
\r
381 pwstat.max = int(round(float(
\r
382 player_events['acc-' + weapon_cd + '-fired'])))
\r
383 if 'acc-' + weapon_cd + '-cnt-hit' in player_events:
\r
384 pwstat.hit = int(round(float(
\r
385 player_events['acc-' + weapon_cd + '-cnt-hit'])))
\r
386 if 'acc-' + weapon_cd + '-hit' in player_events:
\r
387 pwstat.actual = int(round(float(
\r
388 player_events['acc-' + weapon_cd + '-hit'])))
\r
389 if 'acc-' + weapon_cd + '-frags' in player_events:
\r
390 pwstat.frags = int(round(float(
\r
391 player_events['acc-' + weapon_cd + '-frags'])))
\r
394 session.add(pwstat)
\r
396 pwstats.append(pwstat)
\r
401 def parse_body(request):
\r
403 Parses the POST request body for a stats submission
\r
405 # storage vars for the request body
\r
408 current_team = None
\r
411 log.debug("----- BEGIN REQUEST BODY -----")
\r
412 log.debug(request.body)
\r
413 log.debug("----- END REQUEST BODY -----")
\r
415 for line in request.body.split('\n'):
\r
417 (key, value) = line.strip().split(' ', 1)
\r
419 # Server (S) and Nick (n) fields can have international characters.
\r
420 # We encode these as UTF-8.
\r
422 value = unicode(value, 'utf-8')
\r
424 if key in 'V' 'T' 'G' 'M' 'S' 'C' 'R' 'W':
\r
425 game_meta[key] = value
\r
428 # if we were working on a player record already, append
\r
429 # it and work on a new one (only set team info)
\r
430 if len(player_events) != 0:
\r
431 players.append(player_events)
\r
434 player_events[key] = value
\r
437 (subkey, subvalue) = value.split(' ', 1)
\r
438 player_events[subkey] = subvalue
\r
440 player_events[key] = value
\r
442 player_events[key] = value
\r
444 # no key/value pair - move on to the next line
\r
447 # add the last player we were working on
\r
448 if len(player_events) > 0:
\r
449 players.append(player_events)
\r
451 return (game_meta, players)
\r
454 def create_player_stats(session=None, player=None, game=None,
\r
455 player_events=None):
\r
457 Creates player game and weapon stats according to what type of player
\r
459 pgstat = create_player_game_stat(session=session,
\r
460 player=player, game=game, player_events=player_events)
\r
462 #TODO: put this into a config setting in the ini file?
\r
463 if not re.search('^bot#\d+$', player_events['P']):
\r
464 create_player_weapon_stats(session=session,
\r
465 player=player, game=game, pgstat=pgstat,
\r
466 player_events=player_events)
\r
469 def stats_submit(request):
\r
471 Entry handler for POST stats submissions.
\r
474 session = DBSession()
\r
476 (idfp, status) = verify_request(request)
\r
478 raise pyramid.httpexceptions.HTTPUnauthorized
\r
480 (game_meta, players) = parse_body(request)
\r
482 if not has_required_metadata(game_meta):
\r
483 log.debug("Required game meta fields missing. "\
\r
485 raise pyramid.exceptions.HTTPUnprocessableEntity
\r
487 if not is_supported_gametype(game_meta['G']):
\r
488 raise pyramid.httpexceptions.HTTPOk
\r
490 if not has_minimum_real_players(request.registry.settings, players):
\r
491 log.debug("The number of real players is below the minimum. " +
\r
492 "Stats will be ignored.")
\r
493 raise pyramid.httpexceptions.HTTPOk
\r
495 server = get_or_create_server(session=session, hashkey=idfp,
\r
496 name=game_meta['S'])
\r
498 gmap = get_or_create_map(session=session, name=game_meta['M'])
\r
501 game = create_game(session=session,
\r
502 start_dt=datetime.datetime(
\r
503 *time.gmtime(float(game_meta['T']))[:6]),
\r
504 server_id=server.server_id, game_type_cd=game_meta['G'],
\r
505 map_id=gmap.map_id)
\r
508 # find or create a record for each player
\r
509 # and add stats for each if they were present at the end
\r
511 for player_events in players:
\r
512 if 'n' in player_events:
\r
513 nick = player_events['n']
\r
517 if 'matches' in player_events and 'scoreboardvalid' \
\r
519 player = get_or_create_player(session=session,
\r
520 hashkey=player_events['P'], nick=nick)
\r
521 log.debug('Creating stats for %s' % player_events['P'])
\r
522 create_player_stats(session=session, player=player, game=game,
\r
523 player_events=player_events)
\r
526 log.debug('Success! Stats recorded.')
\r
527 return Response('200 OK')
\r
528 except Exception as e:
\r