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
15 def is_verified_request(request):
\r
16 (idfp, status) = d0_blind_id_verify(
\r
17 sig=request.headers['X-D0-Blind-Id-Detached-Signature'],
\r
19 postdata=request.body)
\r
21 log.debug('\nidfp: {0}\nstatus: {1}'.format(idfp, status))
\r
29 def has_minimum_real_players(player_events):
\r
31 Determines if the collection of player events has enough "real" players
\r
32 to store in the database. The minimum setting comes from the config file
\r
33 under the setting xonstat.minimum_real_players.
\r
35 flg_has_min_real_players = True
\r
37 settings = get_current_registry().settings
\r
39 minimum_required_players = int(
\r
40 settings['xonstat.minimum_required_players'])
\r
42 minimum_required_players = 2
\r
45 for events in player_events:
\r
46 if is_real_player(events):
\r
49 #TODO: put this into a config setting in the ini file?
\r
50 if real_players < minimum_required_players:
\r
51 flg_has_min_real_players = False
\r
53 return flg_has_min_real_players
\r
56 def has_required_metadata(metadata):
\r
58 Determines if a give set of metadata has enough data to create a game,
\r
59 server, and map with.
\r
61 flg_has_req_metadata = True
\r
63 if 'T' not in metadata or\
\r
64 'G' not in metadata or\
\r
65 'M' not in metadata or\
\r
66 'S' not in metadata:
\r
67 flg_has_req_metadata = False
\r
69 return flg_has_req_metadata
\r
72 def is_real_player(events):
\r
74 Determines if a given set of player events correspond with a player who
\r
76 1) is not a bot (P event does not look like a bot)
\r
77 2) played in the game (matches 1)
\r
78 3) was present at the end of the game (scoreboardvalid 1)
\r
80 Returns True if the player meets the above conditions, and false otherwise.
\r
84 if not events['P'].startswith('bot'):
\r
85 # removing 'joins' here due to bug, but it should be here
\r
86 if 'matches' in events and 'scoreboardvalid' in events:
\r
92 def register_new_nick(session, player, new_nick):
\r
94 Change the player record's nick to the newly found nick. Store the old
\r
95 nick in the player_nicks table for that player.
\r
97 session - SQLAlchemy database session factory
\r
98 player - player record whose nick is changing
\r
99 new_nick - the new nickname
\r
101 # see if that nick already exists
\r
102 stripped_nick = strip_colors(player.nick)
\r
104 player_nick = session.query(PlayerNick).filter_by(
\r
105 player_id=player.player_id, stripped_nick=stripped_nick).one()
\r
106 except NoResultFound, e:
\r
107 # player_id/stripped_nick not found, create one
\r
108 # but we don't store "Anonymous Player #N"
\r
109 if not re.search('^Anonymous Player #\d+$', player.nick):
\r
110 player_nick = PlayerNick()
\r
111 player_nick.player_id = player.player_id
\r
112 player_nick.stripped_nick = stripped_nick
\r
113 player_nick.nick = player.nick
\r
114 session.add(player_nick)
\r
116 # We change to the new nick regardless
\r
117 player.nick = new_nick
\r
118 session.add(player)
\r
121 def get_or_create_server(session=None, name=None):
\r
123 Find a server by name or create one if not found. Parameters:
\r
125 session - SQLAlchemy database session factory
\r
126 name - server name of the server to be found or created
\r
129 # find one by that name, if it exists
\r
130 server = session.query(Server).filter_by(name=name).one()
\r
131 log.debug("Found server id {0}: {1}".format(
\r
132 server.server_id, server.name.encode('utf-8')))
\r
133 except NoResultFound, e:
\r
134 server = Server(name=name)
\r
135 session.add(server)
\r
137 log.debug("Created server id {0}: {1}".format(
\r
138 server.server_id, server.name.encode('utf-8')))
\r
139 except MultipleResultsFound, e:
\r
140 # multiple found, so use the first one but warn
\r
142 servers = session.query(Server).filter_by(name=name).order_by(
\r
143 Server.server_id).all()
\r
144 server = servers[0]
\r
145 log.debug("Created server id {0}: {1} but found \
\r
147 server.server_id, server.name.encode('utf-8')))
\r
151 def get_or_create_map(session=None, name=None):
\r
153 Find a map by name or create one if not found. Parameters:
\r
155 session - SQLAlchemy database session factory
\r
156 name - map name of the map to be found or created
\r
159 # find one by the name, if it exists
\r
160 gmap = session.query(Map).filter_by(name=name).one()
\r
161 log.debug("Found map id {0}: {1}".format(gmap.map_id,
\r
163 except NoResultFound, e:
\r
164 gmap = Map(name=name)
\r
167 log.debug("Created map id {0}: {1}".format(gmap.map_id,
\r
169 except MultipleResultsFound, e:
\r
170 # multiple found, so use the first one but warn
\r
172 gmaps = session.query(Map).filter_by(name=name).order_by(
\r
175 log.debug("Found map id {0}: {1} but found \
\r
176 multiple".format(gmap.map_id, gmap.name))
\r
181 def create_game(session=None, start_dt=None, game_type_cd=None,
\r
182 server_id=None, map_id=None, winner=None):
\r
184 Creates a game. Parameters:
\r
186 session - SQLAlchemy database session factory
\r
187 start_dt - when the game started (datetime object)
\r
188 game_type_cd - the game type of the game being played
\r
189 server_id - server identifier of the server hosting the game
\r
190 map_id - map on which the game was played
\r
191 winner - the team id of the team that won
\r
194 game = Game(start_dt=start_dt, game_type_cd=game_type_cd,
\r
195 server_id=server_id, map_id=map_id, winner=winner)
\r
198 log.debug("Created game id {0} on server {1}, map {2} at \
\r
199 {3}".format(game.game_id,
\r
200 server_id, map_id, start_dt))
\r
205 def get_or_create_player(session=None, hashkey=None, nick=None):
\r
207 Finds a player by hashkey or creates a new one (along with a
\r
208 corresponding hashkey entry. Parameters:
\r
210 session - SQLAlchemy database session factory
\r
211 hashkey - hashkey of the player to be found or created
\r
212 nick - nick of the player (in case of a first time create)
\r
215 if re.search('^bot#\d+$', hashkey):
\r
216 player = session.query(Player).filter_by(player_id=1).one()
\r
217 # if we have an untracked player
\r
218 elif re.search('^player#\d+$', hashkey):
\r
219 player = session.query(Player).filter_by(player_id=2).one()
\r
220 # else it is a tracked player
\r
222 # see if the player is already in the database
\r
223 # if not, create one and the hashkey along with it
\r
225 hashkey = session.query(Hashkey).filter_by(
\r
226 hashkey=hashkey).one()
\r
227 player = session.query(Player).filter_by(
\r
228 player_id=hashkey.player_id).one()
\r
229 log.debug("Found existing player {0} with hashkey {1}".format(
\r
230 player.player_id, hashkey.hashkey))
\r
233 session.add(player)
\r
236 # if nick is given to us, use it. If not, use "Anonymous Player"
\r
237 # with a suffix added for uniqueness.
\r
239 player.nick = nick[:128]
\r
241 player.nick = "Anonymous Player #{0}".format(player.player_id)
\r
243 hashkey = Hashkey(player_id=player.player_id, hashkey=hashkey)
\r
244 session.add(hashkey)
\r
245 log.debug("Created player {0} ({2}) with hashkey {1}".format(
\r
246 player.player_id, hashkey.hashkey, player.nick.encode('utf-8')))
\r
250 def create_player_game_stat(session=None, player=None,
\r
251 game=None, player_events=None):
\r
253 Creates game statistics for a given player in a given game. Parameters:
\r
255 session - SQLAlchemy session factory
\r
256 player - Player record of the player who owns the stats
\r
257 game - Game record for the game to which the stats pertain
\r
258 player_events - dictionary for the actual stats that need to be transformed
\r
261 # in here setup default values (e.g. if game type is CTF then
\r
262 # set kills=0, score=0, captures=0, pickups=0, fckills=0, etc
\r
263 # TODO: use game's create date here instead of now()
\r
264 pgstat = PlayerGameStat(create_dt=datetime.datetime.now())
\r
266 # set player id from player record
\r
267 pgstat.player_id = player.player_id
\r
269 #set game id from game record
\r
270 pgstat.game_id = game.game_id
\r
272 # all games have a score
\r
275 if game.game_type_cd == 'dm':
\r
278 pgstat.suicides = 0
\r
279 elif game.game_type_cd == 'ctf':
\r
281 pgstat.captures = 0
\r
285 pgstat.carrier_frags = 0
\r
287 for (key,value) in player_events.items():
\r
288 if key == 'n': pgstat.nick = value[:128]
\r
289 if key == 't': pgstat.team = value
\r
290 if key == 'rank': pgstat.rank = value
\r
291 if key == 'alivetime':
\r
292 pgstat.alivetime = datetime.timedelta(seconds=int(round(float(value))))
\r
293 if key == 'scoreboard-drops': pgstat.drops = value
\r
294 if key == 'scoreboard-returns': pgstat.returns = value
\r
295 if key == 'scoreboard-fckills': pgstat.carrier_frags = value
\r
296 if key == 'scoreboard-pickups': pgstat.pickups = value
\r
297 if key == 'scoreboard-caps': pgstat.captures = value
\r
298 if key == 'scoreboard-score': pgstat.score = value
\r
299 if key == 'scoreboard-deaths': pgstat.deaths = value
\r
300 if key == 'scoreboard-kills': pgstat.kills = value
\r
301 if key == 'scoreboard-suicides': pgstat.suicides = value
\r
303 # check to see if we had a name, and if
\r
304 # not use the name from the player id
\r
305 if pgstat.nick == None:
\r
306 pgstat.nick = player.nick
\r
308 # if the nick we end up with is different from the one in the
\r
309 # player record, change the nick to reflect the new value
\r
310 if pgstat.nick != player.nick and player.player_id > 1:
\r
311 register_new_nick(session, player, pgstat.nick)
\r
313 # if the player is ranked #1 and it is a team game, set the game's winner
\r
314 # to be the team of that player
\r
315 # FIXME: this is a hack, should be using the 'W' field (not present)
\r
316 if pgstat.rank == '1' and pgstat.team:
\r
317 game.winner = pgstat.team
\r
320 session.add(pgstat)
\r
326 def create_player_weapon_stats(session=None, player=None,
\r
327 game=None, pgstat=None, player_events=None):
\r
329 Creates accuracy records for each weapon used by a given player in a
\r
330 given game. Parameters:
\r
332 session - SQLAlchemy session factory object
\r
333 player - Player record who owns the weapon stats
\r
334 game - Game record in which the stats were created
\r
335 pgstat - Corresponding PlayerGameStat record for these weapon stats
\r
336 player_events - dictionary containing the raw weapon values that need to be
\r
341 for (key,value) in player_events.items():
\r
342 matched = re.search("acc-(.*?)-cnt-fired", key)
\r
344 weapon_cd = matched.group(1)
\r
345 pwstat = PlayerWeaponStat()
\r
346 pwstat.player_id = player.player_id
\r
347 pwstat.game_id = game.game_id
\r
348 pwstat.player_game_stat_id = pgstat.player_game_stat_id
\r
349 pwstat.weapon_cd = weapon_cd
\r
351 if 'n' in player_events:
\r
352 pwstat.nick = player_events['n']
\r
354 pwstat.nick = player_events['P']
\r
356 if 'acc-' + weapon_cd + '-cnt-fired' in player_events:
\r
357 pwstat.fired = int(round(float(
\r
358 player_events['acc-' + weapon_cd + '-cnt-fired'])))
\r
359 if 'acc-' + weapon_cd + '-fired' in player_events:
\r
360 pwstat.max = int(round(float(
\r
361 player_events['acc-' + weapon_cd + '-fired'])))
\r
362 if 'acc-' + weapon_cd + '-cnt-hit' in player_events:
\r
363 pwstat.hit = int(round(float(
\r
364 player_events['acc-' + weapon_cd + '-cnt-hit'])))
\r
365 if 'acc-' + weapon_cd + '-hit' in player_events:
\r
366 pwstat.actual = int(round(float(
\r
367 player_events['acc-' + weapon_cd + '-hit'])))
\r
368 if 'acc-' + weapon_cd + '-frags' in player_events:
\r
369 pwstat.frags = int(round(float(
\r
370 player_events['acc-' + weapon_cd + '-frags'])))
\r
372 session.add(pwstat)
\r
373 pwstats.append(pwstat)
\r
378 def parse_body(request):
\r
380 Parses the POST request body for a stats submission
\r
382 # storage vars for the request body
\r
385 current_team = None
\r
388 log.debug(request.body)
\r
390 for line in request.body.split('\n'):
\r
392 (key, value) = line.strip().split(' ', 1)
\r
394 # Server (S) and Nick (n) fields can have international characters.
\r
395 # We encode these as UTF-8.
\r
397 value = unicode(value, 'utf-8')
\r
399 if key in 'V' 'T' 'G' 'M' 'S' 'C' 'R' 'W':
\r
400 game_meta[key] = value
\r
403 # if we were working on a player record already, append
\r
404 # it and work on a new one (only set team info)
\r
405 if len(player_events) != 0:
\r
406 players.append(player_events)
\r
409 player_events[key] = value
\r
412 (subkey, subvalue) = value.split(' ', 1)
\r
413 player_events[subkey] = subvalue
\r
415 player_events[key] = value
\r
417 player_events[key] = value
\r
419 # no key/value pair - move on to the next line
\r
422 # add the last player we were working on
\r
423 if len(player_events) > 0:
\r
424 players.append(player_events)
\r
426 return (game_meta, players)
\r
429 def create_player_stats(session=None, player=None, game=None,
\r
430 player_events=None):
\r
432 Creates player game and weapon stats according to what type of player
\r
434 pgstat = create_player_game_stat(session=session,
\r
435 player=player, game=game, player_events=player_events)
\r
437 #TODO: put this into a config setting in the ini file?
\r
438 if not re.search('^bot#\d+$', player_events['P']):
\r
439 create_player_weapon_stats(session=session,
\r
440 player=player, game=game, pgstat=pgstat,
\r
441 player_events=player_events)
\r
444 def stats_submit(request):
\r
446 Entry handler for POST stats submissions.
\r
449 if not is_verified_request(request):
\r
450 raise Exception("Request is not verified.")
\r
452 session = DBSession()
\r
454 (game_meta, players) = parse_body(request)
\r
456 if not has_required_metadata(game_meta):
\r
457 log.debug("Required game meta fields (T, G, M, or S) missing. "\
\r
459 raise Exception("Required game meta fields (T, G, M, or S) missing.")
\r
461 if not has_minimum_real_players(players):
\r
462 raise Exception("The number of real players is below the minimum. "\
\r
463 "Stats will be ignored.")
\r
465 server = get_or_create_server(session=session, name=game_meta['S'])
\r
466 gmap = get_or_create_map(session=session, name=game_meta['M'])
\r
468 game = create_game(session=session,
\r
469 start_dt=datetime.datetime(
\r
470 *time.gmtime(float(game_meta['T']))[:6]),
\r
471 server_id=server.server_id, game_type_cd=game_meta['G'],
\r
472 map_id=gmap.map_id)
\r
474 # find or create a record for each player
\r
475 # and add stats for each if they were present at the end
\r
477 for player_events in players:
\r
478 if 'n' in player_events:
\r
479 nick = player_events['n']
\r
483 if 'matches' in player_events and 'scoreboardvalid' \
\r
485 player = get_or_create_player(session=session,
\r
486 hashkey=player_events['P'], nick=nick)
\r
487 log.debug('Creating stats for %s' % player_events['P'])
\r
488 create_player_stats(session=session, player=player, game=game,
\r
489 player_events=player_events)
\r
492 log.debug('Success! Stats recorded.')
\r
493 return Response('200 OK')
\r
494 except Exception as e:
\r