5 from pyramid.response import Response
\r
6 from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound
\r
7 from xonstat.models import *
\r
8 from xonstat.util import strip_colors
\r
10 log = logging.getLogger(__name__)
\r
12 def is_real_player(events):
\r
14 Determines if a given set of player events correspond with a player who
\r
16 1) is not a bot (P event does not look like a bot)
\r
17 2) played in the game (matches 1)
\r
18 3) was present at the end of the game (scoreboardvalid 1)
\r
20 Returns True if the player meets the above conditions, and false otherwise.
\r
24 if not events['P'].startswith('bot'):
\r
25 # removing 'joins' here due to bug, but it should be here
\r
26 if 'matches' in events and 'scoreboardvalid' in events:
\r
32 def register_new_nick(session, player, new_nick):
\r
34 Change the player record's nick to the newly found nick. Store the old
\r
35 nick in the player_nicks table for that player.
\r
37 session - SQLAlchemy database session factory
\r
38 player - player record whose nick is changing
\r
39 new_nick - the new nickname
\r
41 # see if that nick already exists
\r
42 stripped_nick = strip_colors(player.nick)
\r
44 player_nick = session.query(PlayerNick).filter_by(
\r
45 player_id=player.player_id, stripped_nick=stripped_nick).one()
\r
46 except NoResultFound, e:
\r
47 # player_id/stripped_nick not found, create one
\r
48 # but we don't store "Anonymous Player #N"
\r
49 if not re.search('^Anonymous Player #\d+$', player.nick):
\r
50 player_nick = PlayerNick()
\r
51 player_nick.player_id = player.player_id
\r
52 player_nick.stripped_nick = stripped_nick
\r
53 player_nick.nick = player.nick
\r
54 session.add(player_nick)
\r
56 # We change to the new nick regardless
\r
57 player.nick = new_nick
\r
61 def get_or_create_server(session=None, name=None):
\r
63 Find a server by name or create one if not found. Parameters:
\r
65 session - SQLAlchemy database session factory
\r
66 name - server name of the server to be found or created
\r
69 # find one by that name, if it exists
\r
70 server = session.query(Server).filter_by(name=name).one()
\r
71 log.debug("Found server id {0}: {1}".format(
\r
72 server.server_id, server.name.encode('utf-8')))
\r
73 except NoResultFound, e:
\r
74 server = Server(name=name)
\r
77 log.debug("Created server id {0}: {1}".format(
\r
78 server.server_id, server.name.encode('utf-8')))
\r
79 except MultipleResultsFound, e:
\r
80 # multiple found, so use the first one but warn
\r
82 servers = session.query(Server).filter_by(name=name).order_by(
\r
83 Server.server_id).all()
\r
85 log.debug("Created server id {0}: {1} but found \
\r
87 server.server_id, server.name.encode('utf-8')))
\r
91 def get_or_create_map(session=None, name=None):
\r
93 Find a map by name or create one if not found. Parameters:
\r
95 session - SQLAlchemy database session factory
\r
96 name - map name of the map to be found or created
\r
99 # find one by the name, if it exists
\r
100 gmap = session.query(Map).filter_by(name=name).one()
\r
101 log.debug("Found map id {0}: {1}".format(gmap.map_id,
\r
103 except NoResultFound, e:
\r
104 gmap = Map(name=name)
\r
107 log.debug("Created map id {0}: {1}".format(gmap.map_id,
\r
109 except MultipleResultsFound, e:
\r
110 # multiple found, so use the first one but warn
\r
112 gmaps = session.query(Map).filter_by(name=name).order_by(
\r
115 log.debug("Found map id {0}: {1} but found \
\r
116 multiple".format(gmap.map_id, gmap.name))
\r
121 def create_game(session=None, start_dt=None, game_type_cd=None,
\r
122 server_id=None, map_id=None, winner=None):
\r
124 Creates a game. Parameters:
\r
126 session - SQLAlchemy database session factory
\r
127 start_dt - when the game started (datetime object)
\r
128 game_type_cd - the game type of the game being played
\r
129 server_id - server identifier of the server hosting the game
\r
130 map_id - map on which the game was played
\r
131 winner - the team id of the team that won
\r
134 game = Game(start_dt=start_dt, game_type_cd=game_type_cd,
\r
135 server_id=server_id, map_id=map_id, winner=winner)
\r
138 log.debug("Created game id {0} on server {1}, map {2} at \
\r
139 {3}".format(game.game_id,
\r
140 server_id, map_id, start_dt))
\r
145 def get_or_create_player(session=None, hashkey=None, nick=None):
\r
147 Finds a player by hashkey or creates a new one (along with a
\r
148 corresponding hashkey entry. Parameters:
\r
150 session - SQLAlchemy database session factory
\r
151 hashkey - hashkey of the player to be found or created
\r
152 nick - nick of the player (in case of a first time create)
\r
155 if re.search('^bot#\d+$', hashkey):
\r
156 player = session.query(Player).filter_by(player_id=1).one()
\r
157 # if we have an untracked player
\r
158 elif re.search('^player#\d+$', hashkey):
\r
159 player = session.query(Player).filter_by(player_id=2).one()
\r
160 # else it is a tracked player
\r
162 # see if the player is already in the database
\r
163 # if not, create one and the hashkey along with it
\r
165 hashkey = session.query(Hashkey).filter_by(
\r
166 hashkey=hashkey).one()
\r
167 player = session.query(Player).filter_by(
\r
168 player_id=hashkey.player_id).one()
\r
169 log.debug("Found existing player {0} with hashkey {1}".format(
\r
170 player.player_id, hashkey.hashkey))
\r
173 session.add(player)
\r
176 # if nick is given to us, use it. If not, use "Anonymous Player"
\r
177 # with a suffix added for uniqueness.
\r
179 player.nick = nick[:128]
\r
181 player.nick = "Anonymous Player #{0}".format(player.player_id)
\r
183 hashkey = Hashkey(player_id=player.player_id, hashkey=hashkey)
\r
184 session.add(hashkey)
\r
185 log.debug("Created player {0} ({2}) with hashkey {1}".format(
\r
186 player.player_id, hashkey.hashkey, player.nick.encode('utf-8')))
\r
190 def create_player_game_stat(session=None, player=None,
\r
191 game=None, player_events=None):
\r
193 Creates game statistics for a given player in a given game. Parameters:
\r
195 session - SQLAlchemy session factory
\r
196 player - Player record of the player who owns the stats
\r
197 game - Game record for the game to which the stats pertain
\r
198 player_events - dictionary for the actual stats that need to be transformed
\r
201 # in here setup default values (e.g. if game type is CTF then
\r
202 # set kills=0, score=0, captures=0, pickups=0, fckills=0, etc
\r
203 # TODO: use game's create date here instead of now()
\r
204 pgstat = PlayerGameStat(create_dt=datetime.datetime.now())
\r
206 # set player id from player record
\r
207 pgstat.player_id = player.player_id
\r
209 #set game id from game record
\r
210 pgstat.game_id = game.game_id
\r
212 # all games have a score
\r
215 if game.game_type_cd == 'dm':
\r
218 pgstat.suicides = 0
\r
219 elif game.game_type_cd == 'ctf':
\r
221 pgstat.captures = 0
\r
225 pgstat.carrier_frags = 0
\r
227 for (key,value) in player_events.items():
\r
228 if key == 'n': pgstat.nick = value[:128]
\r
229 if key == 't': pgstat.team = value
\r
230 if key == 'rank': pgstat.rank = value
\r
231 if key == 'alivetime':
\r
232 pgstat.alivetime = datetime.timedelta(seconds=int(round(float(value))))
\r
233 if key == 'scoreboard-drops': pgstat.drops = value
\r
234 if key == 'scoreboard-returns': pgstat.returns = value
\r
235 if key == 'scoreboard-fckills': pgstat.carrier_frags = value
\r
236 if key == 'scoreboard-pickups': pgstat.pickups = value
\r
237 if key == 'scoreboard-caps': pgstat.captures = value
\r
238 if key == 'scoreboard-score': pgstat.score = value
\r
239 if key == 'scoreboard-deaths': pgstat.deaths = value
\r
240 if key == 'scoreboard-kills': pgstat.kills = value
\r
241 if key == 'scoreboard-suicides': pgstat.suicides = value
\r
243 # check to see if we had a name, and if
\r
244 # not use the name from the player id
\r
245 if pgstat.nick == None:
\r
246 pgstat.nick = player.nick
\r
248 # if the nick we end up with is different from the one in the
\r
249 # player record, change the nick to reflect the new value
\r
250 if pgstat.nick != player.nick and player.player_id > 1:
\r
251 register_new_nick(session, player, pgstat.nick)
\r
253 # if the player is ranked #1 and it is a team game, set the game's winner
\r
254 # to be the team of that player
\r
255 # FIXME: this is a hack, should be using the 'W' field (not present)
\r
256 if pgstat.rank == '1' and pgstat.team:
\r
257 game.winner = pgstat.team
\r
260 session.add(pgstat)
\r
266 def create_player_weapon_stats(session=None, player=None,
\r
267 game=None, pgstat=None, player_events=None):
\r
269 Creates accuracy records for each weapon used by a given player in a
\r
270 given game. Parameters:
\r
272 session - SQLAlchemy session factory object
\r
273 player - Player record who owns the weapon stats
\r
274 game - Game record in which the stats were created
\r
275 pgstat - Corresponding PlayerGameStat record for these weapon stats
\r
276 player_events - dictionary containing the raw weapon values that need to be
\r
281 for (key,value) in player_events.items():
\r
282 matched = re.search("acc-(.*?)-cnt-fired", key)
\r
284 weapon_cd = matched.group(1)
\r
285 pwstat = PlayerWeaponStat()
\r
286 pwstat.player_id = player.player_id
\r
287 pwstat.game_id = game.game_id
\r
288 pwstat.player_game_stat_id = pgstat.player_game_stat_id
\r
289 pwstat.weapon_cd = weapon_cd
\r
291 if 'n' in player_events:
\r
292 pwstat.nick = player_events['n']
\r
294 pwstat.nick = player_events['P']
\r
296 if 'acc-' + weapon_cd + '-cnt-fired' in player_events:
\r
297 pwstat.fired = int(round(float(
\r
298 player_events['acc-' + weapon_cd + '-cnt-fired'])))
\r
299 if 'acc-' + weapon_cd + '-fired' in player_events:
\r
300 pwstat.max = int(round(float(
\r
301 player_events['acc-' + weapon_cd + '-fired'])))
\r
302 if 'acc-' + weapon_cd + '-cnt-hit' in player_events:
\r
303 pwstat.hit = int(round(float(
\r
304 player_events['acc-' + weapon_cd + '-cnt-hit'])))
\r
305 if 'acc-' + weapon_cd + '-hit' in player_events:
\r
306 pwstat.actual = int(round(float(
\r
307 player_events['acc-' + weapon_cd + '-hit'])))
\r
308 if 'acc-' + weapon_cd + '-frags' in player_events:
\r
309 pwstat.frags = int(round(float(
\r
310 player_events['acc-' + weapon_cd + '-frags'])))
\r
312 session.add(pwstat)
\r
313 pwstats.append(pwstat)
\r
318 def parse_body(request):
\r
320 Parses the POST request body for a stats submission
\r
322 # storage vars for the request body
\r
325 current_team = None
\r
328 log.debug(request.body)
\r
330 for line in request.body.split('\n'):
\r
332 (key, value) = line.strip().split(' ', 1)
\r
334 # Server (S) and Nick (n) fields can have international characters.
\r
335 # We encode these as UTF-8.
\r
337 value = unicode(value, 'utf-8')
\r
339 if key in 'V' 'T' 'G' 'M' 'S' 'C' 'R' 'W':
\r
340 game_meta[key] = value
\r
343 # if we were working on a player record already, append
\r
344 # it and work on a new one (only set team info)
\r
345 if len(player_events) != 0:
\r
346 players.append(player_events)
\r
349 player_events[key] = value
\r
352 (subkey, subvalue) = value.split(' ', 1)
\r
353 player_events[subkey] = subvalue
\r
355 player_events[key] = value
\r
357 player_events[key] = value
\r
359 # no key/value pair - move on to the next line
\r
362 # add the last player we were working on
\r
363 if len(player_events) > 0:
\r
364 players.append(player_events)
\r
366 return (game_meta, players)
\r
369 def create_player_stats(session=None, player=None, game=None,
\r
370 player_events=None):
\r
372 Creates player game and weapon stats according to what type of player
\r
374 pgstat = create_player_game_stat(session=session,
\r
375 player=player, game=game, player_events=player_events)
\r
377 #TODO: put this into a config setting in the ini file?
\r
378 if not re.search('^bot#\d+$', player_events['P']):
\r
379 create_player_weapon_stats(session=session,
\r
380 player=player, game=game, pgstat=pgstat,
\r
381 player_events=player_events)
\r
384 def stats_submit(request):
\r
386 Entry handler for POST stats submissions.
\r
389 session = DBSession()
\r
391 (game_meta, players) = parse_body(request)
\r
393 # verify required metadata is present
\r
394 if 'T' not in game_meta or\
\r
395 'G' not in game_meta or\
\r
396 'M' not in game_meta or\
\r
397 'S' not in game_meta:
\r
398 log.debug("Required game meta fields (T, G, M, or S) missing. "\
\r
400 raise Exception("Required game meta fields (T, G, M, or S) missing.")
\r
403 for events in players:
\r
404 if is_real_player(events):
\r
407 #TODO: put this into a config setting in the ini file?
\r
408 if real_players < 1:
\r
409 raise Exception("The number of real players is below the minimum. "\
\r
410 "Stats will be ignored.")
\r
412 server = get_or_create_server(session=session, name=game_meta['S'])
\r
413 gmap = get_or_create_map(session=session, name=game_meta['M'])
\r
415 if 'W' in game_meta:
\r
416 winner = game_meta['W']
\r
420 game = create_game(session=session,
\r
421 start_dt=datetime.datetime(
\r
422 *time.gmtime(float(game_meta['T']))[:6]),
\r
423 server_id=server.server_id, game_type_cd=game_meta['G'],
\r
424 map_id=gmap.map_id, winner=winner)
\r
426 # find or create a record for each player
\r
427 # and add stats for each if they were present at the end
\r
429 for player_events in players:
\r
430 if 'n' in player_events:
\r
431 nick = player_events['n']
\r
435 if 'matches' in player_events and 'scoreboardvalid' \
\r
437 player = get_or_create_player(session=session,
\r
438 hashkey=player_events['P'], nick=nick)
\r
439 log.debug('Creating stats for %s' % player_events['P'])
\r
440 create_player_stats(session=session, player=player, game=game,
\r
441 player_events=player_events)
\r
444 log.debug('Success! Stats recorded.')
\r
445 return Response('200 OK')
\r
446 except Exception as e:
\r