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
13 def has_minimum_real_players(player_events):
\r
15 Determines if the collection of player events has enough "real" players
\r
16 to store in the database. The minimum setting comes from the config file
\r
17 under the setting xonstat.minimum_real_players.
\r
19 flg_has_min_real_players = True
\r
22 for events in player_events:
\r
23 if is_real_player(events):
\r
26 #TODO: put this into a config setting in the ini file?
\r
27 if real_players < 1:
\r
28 flg_has_min_real_players = False
\r
30 return flg_has_min_real_players
\r
33 def has_required_metadata(metadata):
\r
35 Determines if a give set of metadata has enough data to create a game,
\r
36 server, and map with.
\r
38 flg_has_req_metadata = True
\r
40 if 'T' not in game_meta or\
\r
41 'G' not in game_meta or\
\r
42 'M' not in game_meta or\
\r
43 'S' not in game_meta:
\r
44 flg_has_req_metadata = False
\r
46 return flg_has_req_metadata
\r
49 def is_real_player(events):
\r
51 Determines if a given set of player events correspond with a player who
\r
53 1) is not a bot (P event does not look like a bot)
\r
54 2) played in the game (matches 1)
\r
55 3) was present at the end of the game (scoreboardvalid 1)
\r
57 Returns True if the player meets the above conditions, and false otherwise.
\r
61 if not events['P'].startswith('bot'):
\r
62 # removing 'joins' here due to bug, but it should be here
\r
63 if 'matches' in events and 'scoreboardvalid' in events:
\r
69 def register_new_nick(session, player, new_nick):
\r
71 Change the player record's nick to the newly found nick. Store the old
\r
72 nick in the player_nicks table for that player.
\r
74 session - SQLAlchemy database session factory
\r
75 player - player record whose nick is changing
\r
76 new_nick - the new nickname
\r
78 # see if that nick already exists
\r
79 stripped_nick = strip_colors(player.nick)
\r
81 player_nick = session.query(PlayerNick).filter_by(
\r
82 player_id=player.player_id, stripped_nick=stripped_nick).one()
\r
83 except NoResultFound, e:
\r
84 # player_id/stripped_nick not found, create one
\r
85 # but we don't store "Anonymous Player #N"
\r
86 if not re.search('^Anonymous Player #\d+$', player.nick):
\r
87 player_nick = PlayerNick()
\r
88 player_nick.player_id = player.player_id
\r
89 player_nick.stripped_nick = stripped_nick
\r
90 player_nick.nick = player.nick
\r
91 session.add(player_nick)
\r
93 # We change to the new nick regardless
\r
94 player.nick = new_nick
\r
98 def get_or_create_server(session=None, name=None):
\r
100 Find a server by name or create one if not found. Parameters:
\r
102 session - SQLAlchemy database session factory
\r
103 name - server name of the server to be found or created
\r
106 # find one by that name, if it exists
\r
107 server = session.query(Server).filter_by(name=name).one()
\r
108 log.debug("Found server id {0}: {1}".format(
\r
109 server.server_id, server.name.encode('utf-8')))
\r
110 except NoResultFound, e:
\r
111 server = Server(name=name)
\r
112 session.add(server)
\r
114 log.debug("Created server id {0}: {1}".format(
\r
115 server.server_id, server.name.encode('utf-8')))
\r
116 except MultipleResultsFound, e:
\r
117 # multiple found, so use the first one but warn
\r
119 servers = session.query(Server).filter_by(name=name).order_by(
\r
120 Server.server_id).all()
\r
121 server = servers[0]
\r
122 log.debug("Created server id {0}: {1} but found \
\r
124 server.server_id, server.name.encode('utf-8')))
\r
128 def get_or_create_map(session=None, name=None):
\r
130 Find a map by name or create one if not found. Parameters:
\r
132 session - SQLAlchemy database session factory
\r
133 name - map name of the map to be found or created
\r
136 # find one by the name, if it exists
\r
137 gmap = session.query(Map).filter_by(name=name).one()
\r
138 log.debug("Found map id {0}: {1}".format(gmap.map_id,
\r
140 except NoResultFound, e:
\r
141 gmap = Map(name=name)
\r
144 log.debug("Created map id {0}: {1}".format(gmap.map_id,
\r
146 except MultipleResultsFound, e:
\r
147 # multiple found, so use the first one but warn
\r
149 gmaps = session.query(Map).filter_by(name=name).order_by(
\r
152 log.debug("Found map id {0}: {1} but found \
\r
153 multiple".format(gmap.map_id, gmap.name))
\r
158 def create_game(session=None, start_dt=None, game_type_cd=None,
\r
159 server_id=None, map_id=None, winner=None):
\r
161 Creates a game. Parameters:
\r
163 session - SQLAlchemy database session factory
\r
164 start_dt - when the game started (datetime object)
\r
165 game_type_cd - the game type of the game being played
\r
166 server_id - server identifier of the server hosting the game
\r
167 map_id - map on which the game was played
\r
168 winner - the team id of the team that won
\r
171 game = Game(start_dt=start_dt, game_type_cd=game_type_cd,
\r
172 server_id=server_id, map_id=map_id, winner=winner)
\r
175 log.debug("Created game id {0} on server {1}, map {2} at \
\r
176 {3}".format(game.game_id,
\r
177 server_id, map_id, start_dt))
\r
182 def get_or_create_player(session=None, hashkey=None, nick=None):
\r
184 Finds a player by hashkey or creates a new one (along with a
\r
185 corresponding hashkey entry. Parameters:
\r
187 session - SQLAlchemy database session factory
\r
188 hashkey - hashkey of the player to be found or created
\r
189 nick - nick of the player (in case of a first time create)
\r
192 if re.search('^bot#\d+$', hashkey):
\r
193 player = session.query(Player).filter_by(player_id=1).one()
\r
194 # if we have an untracked player
\r
195 elif re.search('^player#\d+$', hashkey):
\r
196 player = session.query(Player).filter_by(player_id=2).one()
\r
197 # else it is a tracked player
\r
199 # see if the player is already in the database
\r
200 # if not, create one and the hashkey along with it
\r
202 hashkey = session.query(Hashkey).filter_by(
\r
203 hashkey=hashkey).one()
\r
204 player = session.query(Player).filter_by(
\r
205 player_id=hashkey.player_id).one()
\r
206 log.debug("Found existing player {0} with hashkey {1}".format(
\r
207 player.player_id, hashkey.hashkey))
\r
210 session.add(player)
\r
213 # if nick is given to us, use it. If not, use "Anonymous Player"
\r
214 # with a suffix added for uniqueness.
\r
216 player.nick = nick[:128]
\r
218 player.nick = "Anonymous Player #{0}".format(player.player_id)
\r
220 hashkey = Hashkey(player_id=player.player_id, hashkey=hashkey)
\r
221 session.add(hashkey)
\r
222 log.debug("Created player {0} ({2}) with hashkey {1}".format(
\r
223 player.player_id, hashkey.hashkey, player.nick.encode('utf-8')))
\r
227 def create_player_game_stat(session=None, player=None,
\r
228 game=None, player_events=None):
\r
230 Creates game statistics for a given player in a given game. Parameters:
\r
232 session - SQLAlchemy session factory
\r
233 player - Player record of the player who owns the stats
\r
234 game - Game record for the game to which the stats pertain
\r
235 player_events - dictionary for the actual stats that need to be transformed
\r
238 # in here setup default values (e.g. if game type is CTF then
\r
239 # set kills=0, score=0, captures=0, pickups=0, fckills=0, etc
\r
240 # TODO: use game's create date here instead of now()
\r
241 pgstat = PlayerGameStat(create_dt=datetime.datetime.now())
\r
243 # set player id from player record
\r
244 pgstat.player_id = player.player_id
\r
246 #set game id from game record
\r
247 pgstat.game_id = game.game_id
\r
249 # all games have a score
\r
252 if game.game_type_cd == 'dm':
\r
255 pgstat.suicides = 0
\r
256 elif game.game_type_cd == 'ctf':
\r
258 pgstat.captures = 0
\r
262 pgstat.carrier_frags = 0
\r
264 for (key,value) in player_events.items():
\r
265 if key == 'n': pgstat.nick = value[:128]
\r
266 if key == 't': pgstat.team = value
\r
267 if key == 'rank': pgstat.rank = value
\r
268 if key == 'alivetime':
\r
269 pgstat.alivetime = datetime.timedelta(seconds=int(round(float(value))))
\r
270 if key == 'scoreboard-drops': pgstat.drops = value
\r
271 if key == 'scoreboard-returns': pgstat.returns = value
\r
272 if key == 'scoreboard-fckills': pgstat.carrier_frags = value
\r
273 if key == 'scoreboard-pickups': pgstat.pickups = value
\r
274 if key == 'scoreboard-caps': pgstat.captures = value
\r
275 if key == 'scoreboard-score': pgstat.score = value
\r
276 if key == 'scoreboard-deaths': pgstat.deaths = value
\r
277 if key == 'scoreboard-kills': pgstat.kills = value
\r
278 if key == 'scoreboard-suicides': pgstat.suicides = value
\r
280 # check to see if we had a name, and if
\r
281 # not use the name from the player id
\r
282 if pgstat.nick == None:
\r
283 pgstat.nick = player.nick
\r
285 # if the nick we end up with is different from the one in the
\r
286 # player record, change the nick to reflect the new value
\r
287 if pgstat.nick != player.nick and player.player_id > 1:
\r
288 register_new_nick(session, player, pgstat.nick)
\r
290 # if the player is ranked #1 and it is a team game, set the game's winner
\r
291 # to be the team of that player
\r
292 # FIXME: this is a hack, should be using the 'W' field (not present)
\r
293 if pgstat.rank == '1' and pgstat.team:
\r
294 game.winner = pgstat.team
\r
297 session.add(pgstat)
\r
303 def create_player_weapon_stats(session=None, player=None,
\r
304 game=None, pgstat=None, player_events=None):
\r
306 Creates accuracy records for each weapon used by a given player in a
\r
307 given game. Parameters:
\r
309 session - SQLAlchemy session factory object
\r
310 player - Player record who owns the weapon stats
\r
311 game - Game record in which the stats were created
\r
312 pgstat - Corresponding PlayerGameStat record for these weapon stats
\r
313 player_events - dictionary containing the raw weapon values that need to be
\r
318 for (key,value) in player_events.items():
\r
319 matched = re.search("acc-(.*?)-cnt-fired", key)
\r
321 weapon_cd = matched.group(1)
\r
322 pwstat = PlayerWeaponStat()
\r
323 pwstat.player_id = player.player_id
\r
324 pwstat.game_id = game.game_id
\r
325 pwstat.player_game_stat_id = pgstat.player_game_stat_id
\r
326 pwstat.weapon_cd = weapon_cd
\r
328 if 'n' in player_events:
\r
329 pwstat.nick = player_events['n']
\r
331 pwstat.nick = player_events['P']
\r
333 if 'acc-' + weapon_cd + '-cnt-fired' in player_events:
\r
334 pwstat.fired = int(round(float(
\r
335 player_events['acc-' + weapon_cd + '-cnt-fired'])))
\r
336 if 'acc-' + weapon_cd + '-fired' in player_events:
\r
337 pwstat.max = int(round(float(
\r
338 player_events['acc-' + weapon_cd + '-fired'])))
\r
339 if 'acc-' + weapon_cd + '-cnt-hit' in player_events:
\r
340 pwstat.hit = int(round(float(
\r
341 player_events['acc-' + weapon_cd + '-cnt-hit'])))
\r
342 if 'acc-' + weapon_cd + '-hit' in player_events:
\r
343 pwstat.actual = int(round(float(
\r
344 player_events['acc-' + weapon_cd + '-hit'])))
\r
345 if 'acc-' + weapon_cd + '-frags' in player_events:
\r
346 pwstat.frags = int(round(float(
\r
347 player_events['acc-' + weapon_cd + '-frags'])))
\r
349 session.add(pwstat)
\r
350 pwstats.append(pwstat)
\r
355 def parse_body(request):
\r
357 Parses the POST request body for a stats submission
\r
359 # storage vars for the request body
\r
362 current_team = None
\r
365 log.debug(request.body)
\r
367 for line in request.body.split('\n'):
\r
369 (key, value) = line.strip().split(' ', 1)
\r
371 # Server (S) and Nick (n) fields can have international characters.
\r
372 # We encode these as UTF-8.
\r
374 value = unicode(value, 'utf-8')
\r
376 if key in 'V' 'T' 'G' 'M' 'S' 'C' 'R' 'W':
\r
377 game_meta[key] = value
\r
380 # if we were working on a player record already, append
\r
381 # it and work on a new one (only set team info)
\r
382 if len(player_events) != 0:
\r
383 players.append(player_events)
\r
386 player_events[key] = value
\r
389 (subkey, subvalue) = value.split(' ', 1)
\r
390 player_events[subkey] = subvalue
\r
392 player_events[key] = value
\r
394 player_events[key] = value
\r
396 # no key/value pair - move on to the next line
\r
399 # add the last player we were working on
\r
400 if len(player_events) > 0:
\r
401 players.append(player_events)
\r
403 return (game_meta, players)
\r
406 def create_player_stats(session=None, player=None, game=None,
\r
407 player_events=None):
\r
409 Creates player game and weapon stats according to what type of player
\r
411 pgstat = create_player_game_stat(session=session,
\r
412 player=player, game=game, player_events=player_events)
\r
414 #TODO: put this into a config setting in the ini file?
\r
415 if not re.search('^bot#\d+$', player_events['P']):
\r
416 create_player_weapon_stats(session=session,
\r
417 player=player, game=game, pgstat=pgstat,
\r
418 player_events=player_events)
\r
421 def stats_submit(request):
\r
423 Entry handler for POST stats submissions.
\r
426 session = DBSession()
\r
428 (game_meta, players) = parse_body(request)
\r
430 if not has_required_metadata(game_meta):
\r
431 log.debug("Required game meta fields (T, G, M, or S) missing. "\
\r
433 raise Exception("Required game meta fields (T, G, M, or S) missing.")
\r
435 if not has_minimum_real_players(players):
\r
436 raise Exception("The number of real players is below the minimum. "\
\r
437 "Stats will be ignored.")
\r
439 server = get_or_create_server(session=session, name=game_meta['S'])
\r
440 gmap = get_or_create_map(session=session, name=game_meta['M'])
\r
442 game = create_game(session=session,
\r
443 start_dt=datetime.datetime(
\r
444 *time.gmtime(float(game_meta['T']))[:6]),
\r
445 server_id=server.server_id, game_type_cd=game_meta['G'],
\r
446 map_id=gmap.map_id, winner=winner)
\r
448 # find or create a record for each player
\r
449 # and add stats for each if they were present at the end
\r
451 for player_events in players:
\r
452 if 'n' in player_events:
\r
453 nick = player_events['n']
\r
457 if 'matches' in player_events and 'scoreboardvalid' \
\r
459 player = get_or_create_player(session=session,
\r
460 hashkey=player_events['P'], nick=nick)
\r
461 log.debug('Creating stats for %s' % player_events['P'])
\r
462 create_player_stats(session=session, player=player, game=game,
\r
463 player_events=player_events)
\r
466 log.debug('Success! Stats recorded.')
\r
467 return Response('200 OK')
\r
468 except Exception as e:
\r