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 has_required_metadata(metadata):
\r
14 Determines if a give set of metadata has enough data to create a game,
\r
15 server, and map with.
\r
17 flg_has_req_metadata = True
\r
19 if 'T' not in game_meta or\
\r
20 'G' not in game_meta or\
\r
21 'M' not in game_meta or\
\r
22 'S' not in game_meta:
\r
23 flg_has_req_metadata = False
\r
25 return flg_has_req_metadata
\r
28 def is_real_player(events):
\r
30 Determines if a given set of player events correspond with a player who
\r
32 1) is not a bot (P event does not look like a bot)
\r
33 2) played in the game (matches 1)
\r
34 3) was present at the end of the game (scoreboardvalid 1)
\r
36 Returns True if the player meets the above conditions, and false otherwise.
\r
40 if not events['P'].startswith('bot'):
\r
41 # removing 'joins' here due to bug, but it should be here
\r
42 if 'matches' in events and 'scoreboardvalid' in events:
\r
48 def register_new_nick(session, player, new_nick):
\r
50 Change the player record's nick to the newly found nick. Store the old
\r
51 nick in the player_nicks table for that player.
\r
53 session - SQLAlchemy database session factory
\r
54 player - player record whose nick is changing
\r
55 new_nick - the new nickname
\r
57 # see if that nick already exists
\r
58 stripped_nick = strip_colors(player.nick)
\r
60 player_nick = session.query(PlayerNick).filter_by(
\r
61 player_id=player.player_id, stripped_nick=stripped_nick).one()
\r
62 except NoResultFound, e:
\r
63 # player_id/stripped_nick not found, create one
\r
64 # but we don't store "Anonymous Player #N"
\r
65 if not re.search('^Anonymous Player #\d+$', player.nick):
\r
66 player_nick = PlayerNick()
\r
67 player_nick.player_id = player.player_id
\r
68 player_nick.stripped_nick = stripped_nick
\r
69 player_nick.nick = player.nick
\r
70 session.add(player_nick)
\r
72 # We change to the new nick regardless
\r
73 player.nick = new_nick
\r
77 def get_or_create_server(session=None, name=None):
\r
79 Find a server by name or create one if not found. Parameters:
\r
81 session - SQLAlchemy database session factory
\r
82 name - server name of the server to be found or created
\r
85 # find one by that name, if it exists
\r
86 server = session.query(Server).filter_by(name=name).one()
\r
87 log.debug("Found server id {0}: {1}".format(
\r
88 server.server_id, server.name.encode('utf-8')))
\r
89 except NoResultFound, e:
\r
90 server = Server(name=name)
\r
93 log.debug("Created server id {0}: {1}".format(
\r
94 server.server_id, server.name.encode('utf-8')))
\r
95 except MultipleResultsFound, e:
\r
96 # multiple found, so use the first one but warn
\r
98 servers = session.query(Server).filter_by(name=name).order_by(
\r
99 Server.server_id).all()
\r
100 server = servers[0]
\r
101 log.debug("Created server id {0}: {1} but found \
\r
103 server.server_id, server.name.encode('utf-8')))
\r
107 def get_or_create_map(session=None, name=None):
\r
109 Find a map by name or create one if not found. Parameters:
\r
111 session - SQLAlchemy database session factory
\r
112 name - map name of the map to be found or created
\r
115 # find one by the name, if it exists
\r
116 gmap = session.query(Map).filter_by(name=name).one()
\r
117 log.debug("Found map id {0}: {1}".format(gmap.map_id,
\r
119 except NoResultFound, e:
\r
120 gmap = Map(name=name)
\r
123 log.debug("Created map id {0}: {1}".format(gmap.map_id,
\r
125 except MultipleResultsFound, e:
\r
126 # multiple found, so use the first one but warn
\r
128 gmaps = session.query(Map).filter_by(name=name).order_by(
\r
131 log.debug("Found map id {0}: {1} but found \
\r
132 multiple".format(gmap.map_id, gmap.name))
\r
137 def create_game(session=None, start_dt=None, game_type_cd=None,
\r
138 server_id=None, map_id=None, winner=None):
\r
140 Creates a game. Parameters:
\r
142 session - SQLAlchemy database session factory
\r
143 start_dt - when the game started (datetime object)
\r
144 game_type_cd - the game type of the game being played
\r
145 server_id - server identifier of the server hosting the game
\r
146 map_id - map on which the game was played
\r
147 winner - the team id of the team that won
\r
150 game = Game(start_dt=start_dt, game_type_cd=game_type_cd,
\r
151 server_id=server_id, map_id=map_id, winner=winner)
\r
154 log.debug("Created game id {0} on server {1}, map {2} at \
\r
155 {3}".format(game.game_id,
\r
156 server_id, map_id, start_dt))
\r
161 def get_or_create_player(session=None, hashkey=None, nick=None):
\r
163 Finds a player by hashkey or creates a new one (along with a
\r
164 corresponding hashkey entry. Parameters:
\r
166 session - SQLAlchemy database session factory
\r
167 hashkey - hashkey of the player to be found or created
\r
168 nick - nick of the player (in case of a first time create)
\r
171 if re.search('^bot#\d+$', hashkey):
\r
172 player = session.query(Player).filter_by(player_id=1).one()
\r
173 # if we have an untracked player
\r
174 elif re.search('^player#\d+$', hashkey):
\r
175 player = session.query(Player).filter_by(player_id=2).one()
\r
176 # else it is a tracked player
\r
178 # see if the player is already in the database
\r
179 # if not, create one and the hashkey along with it
\r
181 hashkey = session.query(Hashkey).filter_by(
\r
182 hashkey=hashkey).one()
\r
183 player = session.query(Player).filter_by(
\r
184 player_id=hashkey.player_id).one()
\r
185 log.debug("Found existing player {0} with hashkey {1}".format(
\r
186 player.player_id, hashkey.hashkey))
\r
189 session.add(player)
\r
192 # if nick is given to us, use it. If not, use "Anonymous Player"
\r
193 # with a suffix added for uniqueness.
\r
195 player.nick = nick[:128]
\r
197 player.nick = "Anonymous Player #{0}".format(player.player_id)
\r
199 hashkey = Hashkey(player_id=player.player_id, hashkey=hashkey)
\r
200 session.add(hashkey)
\r
201 log.debug("Created player {0} ({2}) with hashkey {1}".format(
\r
202 player.player_id, hashkey.hashkey, player.nick.encode('utf-8')))
\r
206 def create_player_game_stat(session=None, player=None,
\r
207 game=None, player_events=None):
\r
209 Creates game statistics for a given player in a given game. Parameters:
\r
211 session - SQLAlchemy session factory
\r
212 player - Player record of the player who owns the stats
\r
213 game - Game record for the game to which the stats pertain
\r
214 player_events - dictionary for the actual stats that need to be transformed
\r
217 # in here setup default values (e.g. if game type is CTF then
\r
218 # set kills=0, score=0, captures=0, pickups=0, fckills=0, etc
\r
219 # TODO: use game's create date here instead of now()
\r
220 pgstat = PlayerGameStat(create_dt=datetime.datetime.now())
\r
222 # set player id from player record
\r
223 pgstat.player_id = player.player_id
\r
225 #set game id from game record
\r
226 pgstat.game_id = game.game_id
\r
228 # all games have a score
\r
231 if game.game_type_cd == 'dm':
\r
234 pgstat.suicides = 0
\r
235 elif game.game_type_cd == 'ctf':
\r
237 pgstat.captures = 0
\r
241 pgstat.carrier_frags = 0
\r
243 for (key,value) in player_events.items():
\r
244 if key == 'n': pgstat.nick = value[:128]
\r
245 if key == 't': pgstat.team = value
\r
246 if key == 'rank': pgstat.rank = value
\r
247 if key == 'alivetime':
\r
248 pgstat.alivetime = datetime.timedelta(seconds=int(round(float(value))))
\r
249 if key == 'scoreboard-drops': pgstat.drops = value
\r
250 if key == 'scoreboard-returns': pgstat.returns = value
\r
251 if key == 'scoreboard-fckills': pgstat.carrier_frags = value
\r
252 if key == 'scoreboard-pickups': pgstat.pickups = value
\r
253 if key == 'scoreboard-caps': pgstat.captures = value
\r
254 if key == 'scoreboard-score': pgstat.score = value
\r
255 if key == 'scoreboard-deaths': pgstat.deaths = value
\r
256 if key == 'scoreboard-kills': pgstat.kills = value
\r
257 if key == 'scoreboard-suicides': pgstat.suicides = value
\r
259 # check to see if we had a name, and if
\r
260 # not use the name from the player id
\r
261 if pgstat.nick == None:
\r
262 pgstat.nick = player.nick
\r
264 # if the nick we end up with is different from the one in the
\r
265 # player record, change the nick to reflect the new value
\r
266 if pgstat.nick != player.nick and player.player_id > 1:
\r
267 register_new_nick(session, player, pgstat.nick)
\r
269 # if the player is ranked #1 and it is a team game, set the game's winner
\r
270 # to be the team of that player
\r
271 # FIXME: this is a hack, should be using the 'W' field (not present)
\r
272 if pgstat.rank == '1' and pgstat.team:
\r
273 game.winner = pgstat.team
\r
276 session.add(pgstat)
\r
282 def create_player_weapon_stats(session=None, player=None,
\r
283 game=None, pgstat=None, player_events=None):
\r
285 Creates accuracy records for each weapon used by a given player in a
\r
286 given game. Parameters:
\r
288 session - SQLAlchemy session factory object
\r
289 player - Player record who owns the weapon stats
\r
290 game - Game record in which the stats were created
\r
291 pgstat - Corresponding PlayerGameStat record for these weapon stats
\r
292 player_events - dictionary containing the raw weapon values that need to be
\r
297 for (key,value) in player_events.items():
\r
298 matched = re.search("acc-(.*?)-cnt-fired", key)
\r
300 weapon_cd = matched.group(1)
\r
301 pwstat = PlayerWeaponStat()
\r
302 pwstat.player_id = player.player_id
\r
303 pwstat.game_id = game.game_id
\r
304 pwstat.player_game_stat_id = pgstat.player_game_stat_id
\r
305 pwstat.weapon_cd = weapon_cd
\r
307 if 'n' in player_events:
\r
308 pwstat.nick = player_events['n']
\r
310 pwstat.nick = player_events['P']
\r
312 if 'acc-' + weapon_cd + '-cnt-fired' in player_events:
\r
313 pwstat.fired = int(round(float(
\r
314 player_events['acc-' + weapon_cd + '-cnt-fired'])))
\r
315 if 'acc-' + weapon_cd + '-fired' in player_events:
\r
316 pwstat.max = int(round(float(
\r
317 player_events['acc-' + weapon_cd + '-fired'])))
\r
318 if 'acc-' + weapon_cd + '-cnt-hit' in player_events:
\r
319 pwstat.hit = int(round(float(
\r
320 player_events['acc-' + weapon_cd + '-cnt-hit'])))
\r
321 if 'acc-' + weapon_cd + '-hit' in player_events:
\r
322 pwstat.actual = int(round(float(
\r
323 player_events['acc-' + weapon_cd + '-hit'])))
\r
324 if 'acc-' + weapon_cd + '-frags' in player_events:
\r
325 pwstat.frags = int(round(float(
\r
326 player_events['acc-' + weapon_cd + '-frags'])))
\r
328 session.add(pwstat)
\r
329 pwstats.append(pwstat)
\r
334 def parse_body(request):
\r
336 Parses the POST request body for a stats submission
\r
338 # storage vars for the request body
\r
341 current_team = None
\r
344 log.debug(request.body)
\r
346 for line in request.body.split('\n'):
\r
348 (key, value) = line.strip().split(' ', 1)
\r
350 # Server (S) and Nick (n) fields can have international characters.
\r
351 # We encode these as UTF-8.
\r
353 value = unicode(value, 'utf-8')
\r
355 if key in 'V' 'T' 'G' 'M' 'S' 'C' 'R' 'W':
\r
356 game_meta[key] = value
\r
359 # if we were working on a player record already, append
\r
360 # it and work on a new one (only set team info)
\r
361 if len(player_events) != 0:
\r
362 players.append(player_events)
\r
365 player_events[key] = value
\r
368 (subkey, subvalue) = value.split(' ', 1)
\r
369 player_events[subkey] = subvalue
\r
371 player_events[key] = value
\r
373 player_events[key] = value
\r
375 # no key/value pair - move on to the next line
\r
378 # add the last player we were working on
\r
379 if len(player_events) > 0:
\r
380 players.append(player_events)
\r
382 return (game_meta, players)
\r
385 def create_player_stats(session=None, player=None, game=None,
\r
386 player_events=None):
\r
388 Creates player game and weapon stats according to what type of player
\r
390 pgstat = create_player_game_stat(session=session,
\r
391 player=player, game=game, player_events=player_events)
\r
393 #TODO: put this into a config setting in the ini file?
\r
394 if not re.search('^bot#\d+$', player_events['P']):
\r
395 create_player_weapon_stats(session=session,
\r
396 player=player, game=game, pgstat=pgstat,
\r
397 player_events=player_events)
\r
400 def stats_submit(request):
\r
402 Entry handler for POST stats submissions.
\r
405 session = DBSession()
\r
407 (game_meta, players) = parse_body(request)
\r
409 if not has_required_metadata(game_meta):
\r
410 log.debug("Required game meta fields (T, G, M, or S) missing. "\
\r
412 raise Exception("Required game meta fields (T, G, M, or S) missing.")
\r
415 for events in players:
\r
416 if is_real_player(events):
\r
419 #TODO: put this into a config setting in the ini file?
\r
420 if real_players < 1:
\r
421 raise Exception("The number of real players is below the minimum. "\
\r
422 "Stats will be ignored.")
\r
424 server = get_or_create_server(session=session, name=game_meta['S'])
\r
425 gmap = get_or_create_map(session=session, name=game_meta['M'])
\r
427 game = create_game(session=session,
\r
428 start_dt=datetime.datetime(
\r
429 *time.gmtime(float(game_meta['T']))[:6]),
\r
430 server_id=server.server_id, game_type_cd=game_meta['G'],
\r
431 map_id=gmap.map_id, winner=winner)
\r
433 # find or create a record for each player
\r
434 # and add stats for each if they were present at the end
\r
436 for player_events in players:
\r
437 if 'n' in player_events:
\r
438 nick = player_events['n']
\r
442 if 'matches' in player_events and 'scoreboardvalid' \
\r
444 player = get_or_create_player(session=session,
\r
445 hashkey=player_events['P'], nick=nick)
\r
446 log.debug('Creating stats for %s' % player_events['P'])
\r
447 create_player_stats(session=session, player=player, game=game,
\r
448 player_events=player_events)
\r
451 log.debug('Success! Stats recorded.')
\r
452 return Response('200 OK')
\r
453 except Exception as e:
\r