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.models import *
\r
9 from xonstat.util import strip_colors
\r
11 log = logging.getLogger(__name__)
\r
14 def has_minimum_real_players(player_events):
\r
16 Determines if the collection of player events has enough "real" players
\r
17 to store in the database. The minimum setting comes from the config file
\r
18 under the setting xonstat.minimum_real_players.
\r
20 flg_has_min_real_players = True
\r
22 settings = get_current_registry().settings
\r
24 minimum_required_players = int(
\r
25 settings['xonstat.minimum_required_players'])
\r
27 minimum_required_players = 2
\r
30 for events in player_events:
\r
31 if is_real_player(events):
\r
34 #TODO: put this into a config setting in the ini file?
\r
35 if real_players < minimum_required_players:
\r
36 flg_has_min_real_players = False
\r
38 return flg_has_min_real_players
\r
41 def has_required_metadata(metadata):
\r
43 Determines if a give set of metadata has enough data to create a game,
\r
44 server, and map with.
\r
46 flg_has_req_metadata = True
\r
48 if 'T' not in metadata or\
\r
49 'G' not in metadata or\
\r
50 'M' not in metadata or\
\r
51 'S' not in metadata:
\r
52 flg_has_req_metadata = False
\r
54 return flg_has_req_metadata
\r
57 def is_real_player(events):
\r
59 Determines if a given set of player events correspond with a player who
\r
61 1) is not a bot (P event does not look like a bot)
\r
62 2) played in the game (matches 1)
\r
63 3) was present at the end of the game (scoreboardvalid 1)
\r
65 Returns True if the player meets the above conditions, and false otherwise.
\r
69 if not events['P'].startswith('bot'):
\r
70 # removing 'joins' here due to bug, but it should be here
\r
71 if 'matches' in events and 'scoreboardvalid' in events:
\r
77 def register_new_nick(session, player, new_nick):
\r
79 Change the player record's nick to the newly found nick. Store the old
\r
80 nick in the player_nicks table for that player.
\r
82 session - SQLAlchemy database session factory
\r
83 player - player record whose nick is changing
\r
84 new_nick - the new nickname
\r
86 # see if that nick already exists
\r
87 stripped_nick = strip_colors(player.nick)
\r
89 player_nick = session.query(PlayerNick).filter_by(
\r
90 player_id=player.player_id, stripped_nick=stripped_nick).one()
\r
91 except NoResultFound, e:
\r
92 # player_id/stripped_nick not found, create one
\r
93 # but we don't store "Anonymous Player #N"
\r
94 if not re.search('^Anonymous Player #\d+$', player.nick):
\r
95 player_nick = PlayerNick()
\r
96 player_nick.player_id = player.player_id
\r
97 player_nick.stripped_nick = stripped_nick
\r
98 player_nick.nick = player.nick
\r
99 session.add(player_nick)
\r
101 # We change to the new nick regardless
\r
102 player.nick = new_nick
\r
103 session.add(player)
\r
106 def get_or_create_server(session=None, name=None):
\r
108 Find a server by name or create one if not found. Parameters:
\r
110 session - SQLAlchemy database session factory
\r
111 name - server name of the server to be found or created
\r
114 # find one by that name, if it exists
\r
115 server = session.query(Server).filter_by(name=name).one()
\r
116 log.debug("Found server id {0}: {1}".format(
\r
117 server.server_id, server.name.encode('utf-8')))
\r
118 except NoResultFound, e:
\r
119 server = Server(name=name)
\r
120 session.add(server)
\r
122 log.debug("Created server id {0}: {1}".format(
\r
123 server.server_id, server.name.encode('utf-8')))
\r
124 except MultipleResultsFound, e:
\r
125 # multiple found, so use the first one but warn
\r
127 servers = session.query(Server).filter_by(name=name).order_by(
\r
128 Server.server_id).all()
\r
129 server = servers[0]
\r
130 log.debug("Created server id {0}: {1} but found \
\r
132 server.server_id, server.name.encode('utf-8')))
\r
136 def get_or_create_map(session=None, name=None):
\r
138 Find a map by name or create one if not found. Parameters:
\r
140 session - SQLAlchemy database session factory
\r
141 name - map name of the map to be found or created
\r
144 # find one by the name, if it exists
\r
145 gmap = session.query(Map).filter_by(name=name).one()
\r
146 log.debug("Found map id {0}: {1}".format(gmap.map_id,
\r
148 except NoResultFound, e:
\r
149 gmap = Map(name=name)
\r
152 log.debug("Created map id {0}: {1}".format(gmap.map_id,
\r
154 except MultipleResultsFound, e:
\r
155 # multiple found, so use the first one but warn
\r
157 gmaps = session.query(Map).filter_by(name=name).order_by(
\r
160 log.debug("Found map id {0}: {1} but found \
\r
161 multiple".format(gmap.map_id, gmap.name))
\r
166 def create_game(session=None, start_dt=None, game_type_cd=None,
\r
167 server_id=None, map_id=None, winner=None):
\r
169 Creates a game. Parameters:
\r
171 session - SQLAlchemy database session factory
\r
172 start_dt - when the game started (datetime object)
\r
173 game_type_cd - the game type of the game being played
\r
174 server_id - server identifier of the server hosting the game
\r
175 map_id - map on which the game was played
\r
176 winner - the team id of the team that won
\r
179 game = Game(start_dt=start_dt, game_type_cd=game_type_cd,
\r
180 server_id=server_id, map_id=map_id, winner=winner)
\r
183 log.debug("Created game id {0} on server {1}, map {2} at \
\r
184 {3}".format(game.game_id,
\r
185 server_id, map_id, start_dt))
\r
190 def get_or_create_player(session=None, hashkey=None, nick=None):
\r
192 Finds a player by hashkey or creates a new one (along with a
\r
193 corresponding hashkey entry. Parameters:
\r
195 session - SQLAlchemy database session factory
\r
196 hashkey - hashkey of the player to be found or created
\r
197 nick - nick of the player (in case of a first time create)
\r
200 if re.search('^bot#\d+$', hashkey):
\r
201 player = session.query(Player).filter_by(player_id=1).one()
\r
202 # if we have an untracked player
\r
203 elif re.search('^player#\d+$', hashkey):
\r
204 player = session.query(Player).filter_by(player_id=2).one()
\r
205 # else it is a tracked player
\r
207 # see if the player is already in the database
\r
208 # if not, create one and the hashkey along with it
\r
210 hashkey = session.query(Hashkey).filter_by(
\r
211 hashkey=hashkey).one()
\r
212 player = session.query(Player).filter_by(
\r
213 player_id=hashkey.player_id).one()
\r
214 log.debug("Found existing player {0} with hashkey {1}".format(
\r
215 player.player_id, hashkey.hashkey))
\r
218 session.add(player)
\r
221 # if nick is given to us, use it. If not, use "Anonymous Player"
\r
222 # with a suffix added for uniqueness.
\r
224 player.nick = nick[:128]
\r
226 player.nick = "Anonymous Player #{0}".format(player.player_id)
\r
228 hashkey = Hashkey(player_id=player.player_id, hashkey=hashkey)
\r
229 session.add(hashkey)
\r
230 log.debug("Created player {0} ({2}) with hashkey {1}".format(
\r
231 player.player_id, hashkey.hashkey, player.nick.encode('utf-8')))
\r
235 def create_player_game_stat(session=None, player=None,
\r
236 game=None, player_events=None):
\r
238 Creates game statistics for a given player in a given game. Parameters:
\r
240 session - SQLAlchemy session factory
\r
241 player - Player record of the player who owns the stats
\r
242 game - Game record for the game to which the stats pertain
\r
243 player_events - dictionary for the actual stats that need to be transformed
\r
246 # in here setup default values (e.g. if game type is CTF then
\r
247 # set kills=0, score=0, captures=0, pickups=0, fckills=0, etc
\r
248 # TODO: use game's create date here instead of now()
\r
249 pgstat = PlayerGameStat(create_dt=datetime.datetime.now())
\r
251 # set player id from player record
\r
252 pgstat.player_id = player.player_id
\r
254 #set game id from game record
\r
255 pgstat.game_id = game.game_id
\r
257 # all games have a score
\r
260 if game.game_type_cd == 'dm':
\r
263 pgstat.suicides = 0
\r
264 elif game.game_type_cd == 'ctf':
\r
266 pgstat.captures = 0
\r
270 pgstat.carrier_frags = 0
\r
272 for (key,value) in player_events.items():
\r
273 if key == 'n': pgstat.nick = value[:128]
\r
274 if key == 't': pgstat.team = value
\r
275 if key == 'rank': pgstat.rank = value
\r
276 if key == 'alivetime':
\r
277 pgstat.alivetime = datetime.timedelta(seconds=int(round(float(value))))
\r
278 if key == 'scoreboard-drops': pgstat.drops = value
\r
279 if key == 'scoreboard-returns': pgstat.returns = value
\r
280 if key == 'scoreboard-fckills': pgstat.carrier_frags = value
\r
281 if key == 'scoreboard-pickups': pgstat.pickups = value
\r
282 if key == 'scoreboard-caps': pgstat.captures = value
\r
283 if key == 'scoreboard-score': pgstat.score = value
\r
284 if key == 'scoreboard-deaths': pgstat.deaths = value
\r
285 if key == 'scoreboard-kills': pgstat.kills = value
\r
286 if key == 'scoreboard-suicides': pgstat.suicides = value
\r
288 # check to see if we had a name, and if
\r
289 # not use the name from the player id
\r
290 if pgstat.nick == None:
\r
291 pgstat.nick = player.nick
\r
293 # if the nick we end up with is different from the one in the
\r
294 # player record, change the nick to reflect the new value
\r
295 if pgstat.nick != player.nick and player.player_id > 1:
\r
296 register_new_nick(session, player, pgstat.nick)
\r
298 # if the player is ranked #1 and it is a team game, set the game's winner
\r
299 # to be the team of that player
\r
300 # FIXME: this is a hack, should be using the 'W' field (not present)
\r
301 if pgstat.rank == '1' and pgstat.team:
\r
302 game.winner = pgstat.team
\r
305 session.add(pgstat)
\r
311 def create_player_weapon_stats(session=None, player=None,
\r
312 game=None, pgstat=None, player_events=None):
\r
314 Creates accuracy records for each weapon used by a given player in a
\r
315 given game. Parameters:
\r
317 session - SQLAlchemy session factory object
\r
318 player - Player record who owns the weapon stats
\r
319 game - Game record in which the stats were created
\r
320 pgstat - Corresponding PlayerGameStat record for these weapon stats
\r
321 player_events - dictionary containing the raw weapon values that need to be
\r
326 for (key,value) in player_events.items():
\r
327 matched = re.search("acc-(.*?)-cnt-fired", key)
\r
329 weapon_cd = matched.group(1)
\r
330 pwstat = PlayerWeaponStat()
\r
331 pwstat.player_id = player.player_id
\r
332 pwstat.game_id = game.game_id
\r
333 pwstat.player_game_stat_id = pgstat.player_game_stat_id
\r
334 pwstat.weapon_cd = weapon_cd
\r
336 if 'n' in player_events:
\r
337 pwstat.nick = player_events['n']
\r
339 pwstat.nick = player_events['P']
\r
341 if 'acc-' + weapon_cd + '-cnt-fired' in player_events:
\r
342 pwstat.fired = int(round(float(
\r
343 player_events['acc-' + weapon_cd + '-cnt-fired'])))
\r
344 if 'acc-' + weapon_cd + '-fired' in player_events:
\r
345 pwstat.max = int(round(float(
\r
346 player_events['acc-' + weapon_cd + '-fired'])))
\r
347 if 'acc-' + weapon_cd + '-cnt-hit' in player_events:
\r
348 pwstat.hit = int(round(float(
\r
349 player_events['acc-' + weapon_cd + '-cnt-hit'])))
\r
350 if 'acc-' + weapon_cd + '-hit' in player_events:
\r
351 pwstat.actual = int(round(float(
\r
352 player_events['acc-' + weapon_cd + '-hit'])))
\r
353 if 'acc-' + weapon_cd + '-frags' in player_events:
\r
354 pwstat.frags = int(round(float(
\r
355 player_events['acc-' + weapon_cd + '-frags'])))
\r
357 session.add(pwstat)
\r
358 pwstats.append(pwstat)
\r
363 def parse_body(request):
\r
365 Parses the POST request body for a stats submission
\r
367 # storage vars for the request body
\r
370 current_team = None
\r
373 log.debug(request.body)
\r
375 for line in request.body.split('\n'):
\r
377 (key, value) = line.strip().split(' ', 1)
\r
379 # Server (S) and Nick (n) fields can have international characters.
\r
380 # We encode these as UTF-8.
\r
382 value = unicode(value, 'utf-8')
\r
384 if key in 'V' 'T' 'G' 'M' 'S' 'C' 'R' 'W':
\r
385 game_meta[key] = value
\r
388 # if we were working on a player record already, append
\r
389 # it and work on a new one (only set team info)
\r
390 if len(player_events) != 0:
\r
391 players.append(player_events)
\r
394 player_events[key] = value
\r
397 (subkey, subvalue) = value.split(' ', 1)
\r
398 player_events[subkey] = subvalue
\r
400 player_events[key] = value
\r
402 player_events[key] = value
\r
404 # no key/value pair - move on to the next line
\r
407 # add the last player we were working on
\r
408 if len(player_events) > 0:
\r
409 players.append(player_events)
\r
411 return (game_meta, players)
\r
414 def create_player_stats(session=None, player=None, game=None,
\r
415 player_events=None):
\r
417 Creates player game and weapon stats according to what type of player
\r
419 pgstat = create_player_game_stat(session=session,
\r
420 player=player, game=game, player_events=player_events)
\r
422 #TODO: put this into a config setting in the ini file?
\r
423 if not re.search('^bot#\d+$', player_events['P']):
\r
424 create_player_weapon_stats(session=session,
\r
425 player=player, game=game, pgstat=pgstat,
\r
426 player_events=player_events)
\r
429 def stats_submit(request):
\r
431 Entry handler for POST stats submissions.
\r
434 session = DBSession()
\r
436 (game_meta, players) = parse_body(request)
\r
438 if not has_required_metadata(game_meta):
\r
439 log.debug("Required game meta fields (T, G, M, or S) missing. "\
\r
441 raise Exception("Required game meta fields (T, G, M, or S) missing.")
\r
443 if not has_minimum_real_players(players):
\r
444 raise Exception("The number of real players is below the minimum. "\
\r
445 "Stats will be ignored.")
\r
447 server = get_or_create_server(session=session, name=game_meta['S'])
\r
448 gmap = get_or_create_map(session=session, name=game_meta['M'])
\r
450 game = create_game(session=session,
\r
451 start_dt=datetime.datetime(
\r
452 *time.gmtime(float(game_meta['T']))[:6]),
\r
453 server_id=server.server_id, game_type_cd=game_meta['G'],
\r
454 map_id=gmap.map_id)
\r
456 # find or create a record for each player
\r
457 # and add stats for each if they were present at the end
\r
459 for player_events in players:
\r
460 if 'n' in player_events:
\r
461 nick = player_events['n']
\r
465 if 'matches' in player_events and 'scoreboardvalid' \
\r
467 player = get_or_create_player(session=session,
\r
468 hashkey=player_events['P'], nick=nick)
\r
469 log.debug('Creating stats for %s' % player_events['P'])
\r
470 create_player_stats(session=session, player=player, game=game,
\r
471 player_events=player_events)
\r
474 log.debug('Success! Stats recorded.')
\r
475 return Response('200 OK')
\r
476 except Exception as e:
\r