]> de.git.xonotic.org Git - xonotic/xonstat.git/blob - xonstat/views/submission.py
Do not nick track player #2!
[xonotic/xonstat.git] / xonstat / views / submission.py
1 import datetime\r
2 import logging\r
3 import re\r
4 import time\r
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
11 \r
12 log = logging.getLogger(__name__)\r
13 \r
14 def is_supported_gametype(gametype):\r
15     """Whether a gametype is supported or not"""\r
16     flg_supported = True\r
17 \r
18     if gametype == 'cts' or gametype == 'ca' or gametype == 'lms':\r
19         flg_supported = False\r
20 \r
21     return flg_supported\r
22 \r
23 \r
24 def verify_request(request):\r
25     (idfp, status) = d0_blind_id_verify(\r
26             sig=request.headers['X-D0-Blind-Id-Detached-Signature'],\r
27             querystring='',\r
28             postdata=request.body)\r
29 \r
30     log.debug('\nidfp: {0}\nstatus: {1}'.format(idfp, status))\r
31 \r
32     return (idfp, status)\r
33 \r
34 \r
35 def has_minimum_real_players(player_events):\r
36     """\r
37     Determines if the collection of player events has enough "real" players\r
38     to store in the database. The minimum setting comes from the config file\r
39     under the setting xonstat.minimum_real_players.\r
40     """\r
41     flg_has_min_real_players = True\r
42 \r
43     settings = get_current_registry().settings\r
44     try: \r
45         minimum_required_players = int(\r
46                 settings['xonstat.minimum_required_players'])\r
47     except:\r
48         minimum_required_players = 2\r
49 \r
50     real_players = 0\r
51     for events in player_events:\r
52         if is_real_player(events):\r
53             real_players += 1\r
54 \r
55     #TODO: put this into a config setting in the ini file?\r
56     if real_players < minimum_required_players:\r
57         flg_has_min_real_players = False\r
58 \r
59     return flg_has_min_real_players\r
60 \r
61 \r
62 def has_required_metadata(metadata):\r
63     """\r
64     Determines if a give set of metadata has enough data to create a game,\r
65     server, and map with.\r
66     """\r
67     flg_has_req_metadata = True\r
68 \r
69     if 'T' not in metadata or\\r
70         'G' not in metadata or\\r
71         'M' not in metadata or\\r
72         'S' not in metadata:\r
73             flg_has_req_metadata = False\r
74 \r
75     return flg_has_req_metadata\r
76 \r
77     \r
78 def is_real_player(events):\r
79     """\r
80     Determines if a given set of player events correspond with a player who\r
81     \r
82     1) is not a bot (P event does not look like a bot)\r
83     2) played in the game (matches 1)\r
84     3) was present at the end of the game (scoreboardvalid 1)\r
85 \r
86     Returns True if the player meets the above conditions, and false otherwise.\r
87     """\r
88     flg_is_real = False\r
89 \r
90     if not events['P'].startswith('bot'):\r
91         # removing 'joins' here due to bug, but it should be here\r
92         if 'matches' in events and 'scoreboardvalid' in events:\r
93             flg_is_real = True\r
94 \r
95     return flg_is_real\r
96 \r
97 \r
98 def register_new_nick(session, player, new_nick):\r
99     """\r
100     Change the player record's nick to the newly found nick. Store the old\r
101     nick in the player_nicks table for that player.\r
102 \r
103     session - SQLAlchemy database session factory\r
104     player - player record whose nick is changing\r
105     new_nick - the new nickname\r
106     """\r
107     # see if that nick already exists\r
108     stripped_nick = strip_colors(player.nick)\r
109     try:\r
110         player_nick = session.query(PlayerNick).filter_by(\r
111                 player_id=player.player_id, stripped_nick=stripped_nick).one()\r
112     except NoResultFound, e:\r
113             # player_id/stripped_nick not found, create one\r
114         # but we don't store "Anonymous Player #N"\r
115         if not re.search('^Anonymous Player #\d+$', player.nick):\r
116             player_nick = PlayerNick()\r
117             player_nick.player_id = player.player_id\r
118             player_nick.stripped_nick = stripped_nick\r
119             player_nick.nick = player.nick\r
120             session.add(player_nick)\r
121 \r
122     # We change to the new nick regardless\r
123     player.nick = new_nick\r
124     session.add(player)\r
125 \r
126 \r
127 def get_or_create_server(session=None, name=None, hashkey=None):\r
128     """\r
129     Find a server by name or create one if not found. Parameters:\r
130 \r
131     session - SQLAlchemy database session factory\r
132     name - server name of the server to be found or created\r
133     hashkey - server hashkey\r
134     """\r
135     try:\r
136         # find one by that name, if it exists\r
137         server = session.query(Server).filter_by(name=name).one()\r
138 \r
139         # store new hashkey\r
140         if server.hashkey != hashkey:\r
141             server.hashkey = hashkey\r
142             session.add(server)\r
143 \r
144         log.debug("Found existing server {0}".format(server.server_id))\r
145 \r
146     except MultipleResultsFound, e:\r
147         # multiple found, so also filter by hashkey\r
148         server = session.query(Server).filter_by(name=name).\\r
149                 filter_by(hashkey=hashkey).one()\r
150         log.debug("Found existing server {0}".format(server.server_id))\r
151 \r
152     except NoResultFound, e:\r
153         # not found, create one\r
154         server = Server(name=name, hashkey=hashkey)\r
155         session.add(server)\r
156         session.flush()\r
157         log.debug("Created server {0} with hashkey {1}".format(\r
158             server.server_id, server.hashkey))\r
159 \r
160     return server\r
161 \r
162 \r
163 def get_or_create_map(session=None, name=None):\r
164     """\r
165     Find a map by name or create one if not found. Parameters:\r
166 \r
167     session - SQLAlchemy database session factory\r
168     name - map name of the map to be found or created\r
169     """\r
170     try:\r
171         # find one by the name, if it exists\r
172         gmap = session.query(Map).filter_by(name=name).one()\r
173         log.debug("Found map id {0}: {1}".format(gmap.map_id, \r
174             gmap.name))\r
175     except NoResultFound, e:\r
176         gmap = Map(name=name)\r
177         session.add(gmap)\r
178         session.flush()\r
179         log.debug("Created map id {0}: {1}".format(gmap.map_id,\r
180             gmap.name))\r
181     except MultipleResultsFound, e:\r
182         # multiple found, so use the first one but warn\r
183         log.debug(e)\r
184         gmaps = session.query(Map).filter_by(name=name).order_by(\r
185                 Map.map_id).all()\r
186         gmap = gmaps[0]\r
187         log.debug("Found map id {0}: {1} but found \\r
188                 multiple".format(gmap.map_id, gmap.name))\r
189 \r
190     return gmap\r
191 \r
192 \r
193 def create_game(session=None, start_dt=None, game_type_cd=None, \r
194         server_id=None, map_id=None, winner=None):\r
195     """\r
196     Creates a game. Parameters:\r
197 \r
198     session - SQLAlchemy database session factory\r
199     start_dt - when the game started (datetime object)\r
200     game_type_cd - the game type of the game being played\r
201     server_id - server identifier of the server hosting the game\r
202     map_id - map on which the game was played\r
203     winner - the team id of the team that won\r
204     """\r
205 \r
206     game = Game(start_dt=start_dt, game_type_cd=game_type_cd,\r
207                 server_id=server_id, map_id=map_id, winner=winner)\r
208     session.add(game)\r
209     session.flush()\r
210     log.debug("Created game id {0} on server {1}, map {2} at \\r
211             {3}".format(game.game_id, \r
212                 server_id, map_id, start_dt))\r
213 \r
214     return game\r
215 \r
216 \r
217 def get_or_create_player(session=None, hashkey=None, nick=None):\r
218     """\r
219     Finds a player by hashkey or creates a new one (along with a\r
220     corresponding hashkey entry. Parameters:\r
221 \r
222     session - SQLAlchemy database session factory\r
223     hashkey - hashkey of the player to be found or created\r
224     nick - nick of the player (in case of a first time create)\r
225     """\r
226     # if we have a bot\r
227     if re.search('^bot#\d+$', hashkey):\r
228         player = session.query(Player).filter_by(player_id=1).one()\r
229     # if we have an untracked player\r
230     elif re.search('^player#\d+$', hashkey):\r
231         player = session.query(Player).filter_by(player_id=2).one()\r
232     # else it is a tracked player\r
233     else:\r
234         # see if the player is already in the database\r
235         # if not, create one and the hashkey along with it\r
236         try:\r
237             hashkey = session.query(Hashkey).filter_by(\r
238                     hashkey=hashkey).one()\r
239             player = session.query(Player).filter_by(\r
240                     player_id=hashkey.player_id).one()\r
241             log.debug("Found existing player {0} with hashkey {1}".format(\r
242                 player.player_id, hashkey.hashkey))\r
243         except:\r
244             player = Player()\r
245             session.add(player)\r
246             session.flush()\r
247 \r
248             # if nick is given to us, use it. If not, use "Anonymous Player"\r
249             # with a suffix added for uniqueness.\r
250             if nick:\r
251                 player.nick = nick[:128]\r
252             else:\r
253                 player.nick = "Anonymous Player #{0}".format(player.player_id)\r
254 \r
255             hashkey = Hashkey(player_id=player.player_id, hashkey=hashkey)\r
256             session.add(hashkey)\r
257             log.debug("Created player {0} ({2}) with hashkey {1}".format(\r
258                 player.player_id, hashkey.hashkey, player.nick.encode('utf-8')))\r
259 \r
260     return player\r
261 \r
262 def create_player_game_stat(session=None, player=None, \r
263         game=None, player_events=None):\r
264     """\r
265     Creates game statistics for a given player in a given game. Parameters:\r
266 \r
267     session - SQLAlchemy session factory\r
268     player - Player record of the player who owns the stats\r
269     game - Game record for the game to which the stats pertain\r
270     player_events - dictionary for the actual stats that need to be transformed\r
271     """\r
272 \r
273     # in here setup default values (e.g. if game type is CTF then\r
274     # set kills=0, score=0, captures=0, pickups=0, fckills=0, etc\r
275     # TODO: use game's create date here instead of now()\r
276     pgstat = PlayerGameStat(create_dt=datetime.datetime.now())\r
277 \r
278     # set player id from player record\r
279     pgstat.player_id = player.player_id\r
280 \r
281     #set game id from game record\r
282     pgstat.game_id = game.game_id\r
283 \r
284     # all games have a score\r
285     pgstat.score = 0\r
286 \r
287     if game.game_type_cd == 'dm':\r
288         pgstat.kills = 0\r
289         pgstat.deaths = 0\r
290         pgstat.suicides = 0\r
291     elif game.game_type_cd == 'ctf':\r
292         pgstat.kills = 0\r
293         pgstat.captures = 0\r
294         pgstat.pickups = 0\r
295         pgstat.drops = 0\r
296         pgstat.returns = 0\r
297         pgstat.carrier_frags = 0\r
298 \r
299     for (key,value) in player_events.items():\r
300         if key == 'n': pgstat.nick = value[:128]\r
301         if key == 't': pgstat.team = value\r
302         if key == 'rank': pgstat.rank = value\r
303         if key == 'alivetime': \r
304             pgstat.alivetime = datetime.timedelta(seconds=int(round(float(value))))\r
305         if key == 'scoreboard-drops': pgstat.drops = value\r
306         if key == 'scoreboard-returns': pgstat.returns = value\r
307         if key == 'scoreboard-fckills': pgstat.carrier_frags = value\r
308         if key == 'scoreboard-pickups': pgstat.pickups = value\r
309         if key == 'scoreboard-caps': pgstat.captures = value\r
310         if key == 'scoreboard-score': pgstat.score = value\r
311         if key == 'scoreboard-deaths': pgstat.deaths = value\r
312         if key == 'scoreboard-kills': pgstat.kills = value\r
313         if key == 'scoreboard-suicides': pgstat.suicides = value\r
314 \r
315     # check to see if we had a name, and if \r
316     # not use the name from the player id\r
317     if pgstat.nick == None:\r
318         pgstat.nick = player.nick\r
319 \r
320     # if the nick we end up with is different from the one in the\r
321     # player record, change the nick to reflect the new value\r
322     if pgstat.nick != player.nick and player.player_id > 2:\r
323         register_new_nick(session, player, pgstat.nick)\r
324 \r
325     # if the player is ranked #1 and it is a team game, set the game's winner\r
326     # to be the team of that player\r
327     # FIXME: this is a hack, should be using the 'W' field (not present)\r
328     if pgstat.rank == '1' and pgstat.team:\r
329         game.winner = pgstat.team\r
330         session.add(game)\r
331 \r
332     session.add(pgstat)\r
333     session.flush()\r
334 \r
335     return pgstat\r
336 \r
337 \r
338 def create_player_weapon_stats(session=None, player=None, \r
339         game=None, pgstat=None, player_events=None):\r
340     """\r
341     Creates accuracy records for each weapon used by a given player in a\r
342     given game. Parameters:\r
343 \r
344     session - SQLAlchemy session factory object\r
345     player - Player record who owns the weapon stats\r
346     game - Game record in which the stats were created\r
347     pgstat - Corresponding PlayerGameStat record for these weapon stats\r
348     player_events - dictionary containing the raw weapon values that need to be\r
349         transformed\r
350     """\r
351     pwstats = []\r
352 \r
353     for (key,value) in player_events.items():\r
354         matched = re.search("acc-(.*?)-cnt-fired", key)\r
355         if matched:\r
356             weapon_cd = matched.group(1)\r
357             pwstat = PlayerWeaponStat()\r
358             pwstat.player_id = player.player_id\r
359             pwstat.game_id = game.game_id\r
360             pwstat.player_game_stat_id = pgstat.player_game_stat_id\r
361             pwstat.weapon_cd = weapon_cd\r
362 \r
363             if 'n' in player_events:\r
364                 pwstat.nick = player_events['n']\r
365             else:\r
366                 pwstat.nick = player_events['P']\r
367 \r
368             if 'acc-' + weapon_cd + '-cnt-fired' in player_events:\r
369                 pwstat.fired = int(round(float(\r
370                         player_events['acc-' + weapon_cd + '-cnt-fired'])))\r
371             if 'acc-' + weapon_cd + '-fired' in player_events:\r
372                 pwstat.max = int(round(float(\r
373                         player_events['acc-' + weapon_cd + '-fired'])))\r
374             if 'acc-' + weapon_cd + '-cnt-hit' in player_events:\r
375                 pwstat.hit = int(round(float(\r
376                         player_events['acc-' + weapon_cd + '-cnt-hit'])))\r
377             if 'acc-' + weapon_cd + '-hit' in player_events:\r
378                 pwstat.actual = int(round(float(\r
379                         player_events['acc-' + weapon_cd + '-hit'])))\r
380             if 'acc-' + weapon_cd + '-frags' in player_events:\r
381                 pwstat.frags = int(round(float(\r
382                         player_events['acc-' + weapon_cd + '-frags'])))\r
383 \r
384             session.add(pwstat)\r
385             pwstats.append(pwstat)\r
386 \r
387     return pwstats\r
388 \r
389 \r
390 def parse_body(request):\r
391     """\r
392     Parses the POST request body for a stats submission\r
393     """\r
394     # storage vars for the request body\r
395     game_meta = {}\r
396     player_events = {}\r
397     current_team = None\r
398     players = []\r
399     \r
400     log.debug("----- BEGIN REQUEST BODY -----")\r
401     log.debug(request.body)\r
402     log.debug("----- END REQUEST BODY -----")\r
403 \r
404     for line in request.body.split('\n'):\r
405         try:\r
406             (key, value) = line.strip().split(' ', 1)\r
407 \r
408             # Server (S) and Nick (n) fields can have international characters.\r
409             # We encode these as UTF-8.\r
410             if key in 'S' 'n':\r
411                 value = unicode(value, 'utf-8')\r
412     \r
413             if key in 'V' 'T' 'G' 'M' 'S' 'C' 'R' 'W':\r
414                 game_meta[key] = value\r
415 \r
416             if key == 'P':\r
417                 # if we were working on a player record already, append\r
418                 # it and work on a new one (only set team info)\r
419                 if len(player_events) != 0:\r
420                     players.append(player_events)\r
421                     player_events = {}\r
422     \r
423                 player_events[key] = value\r
424 \r
425             if key == 'e':\r
426                 (subkey, subvalue) = value.split(' ', 1)\r
427                 player_events[subkey] = subvalue\r
428             if key == 'n':\r
429                 player_events[key] = value\r
430             if key == 't':\r
431                 player_events[key] = value\r
432         except:\r
433             # no key/value pair - move on to the next line\r
434             pass\r
435     \r
436     # add the last player we were working on\r
437     if len(player_events) > 0:\r
438         players.append(player_events)\r
439 \r
440     return (game_meta, players)\r
441 \r
442 \r
443 def create_player_stats(session=None, player=None, game=None, \r
444         player_events=None):\r
445     """\r
446     Creates player game and weapon stats according to what type of player\r
447     """\r
448     pgstat = create_player_game_stat(session=session, \r
449         player=player, game=game, player_events=player_events)\r
450 \r
451     #TODO: put this into a config setting in the ini file?\r
452     if not re.search('^bot#\d+$', player_events['P']):\r
453         create_player_weapon_stats(session=session, \r
454             player=player, game=game, pgstat=pgstat,\r
455             player_events=player_events)\r
456     \r
457 \r
458 def stats_submit(request):\r
459     """\r
460     Entry handler for POST stats submissions.\r
461     """\r
462     try:\r
463         session = DBSession()\r
464 \r
465         (idfp, status) = verify_request(request)\r
466         if not idfp:\r
467             raise Exception("Request is not verified.")\r
468 \r
469         (game_meta, players) = parse_body(request)  \r
470     \r
471         if not has_required_metadata(game_meta):\r
472             log.debug("Required game meta fields (T, G, M, or S) missing. "\\r
473                     "Can't continue.")\r
474             raise Exception("Required game meta fields (T, G, M, or S) missing.")\r
475    \r
476         if not is_supported_gametype(game_meta['G']):\r
477             raise Exception("Gametype not supported.")\r
478      \r
479         if not has_minimum_real_players(players):\r
480             raise Exception("The number of real players is below the minimum. "\\r
481                     "Stats will be ignored.")\r
482 \r
483         server = get_or_create_server(session=session, hashkey=idfp, \r
484                 name=game_meta['S'])\r
485 \r
486         gmap = get_or_create_map(session=session, name=game_meta['M'])\r
487 \r
488         game = create_game(session=session, \r
489                 start_dt=datetime.datetime(\r
490                     *time.gmtime(float(game_meta['T']))[:6]), \r
491                 server_id=server.server_id, game_type_cd=game_meta['G'], \r
492                 map_id=gmap.map_id)\r
493     \r
494         # find or create a record for each player\r
495         # and add stats for each if they were present at the end\r
496         # of the game\r
497         for player_events in players:\r
498             if 'n' in player_events:\r
499                 nick = player_events['n']\r
500             else:\r
501                 nick = None\r
502 \r
503             if 'matches' in player_events and 'scoreboardvalid' \\r
504                     in player_events:\r
505                 player = get_or_create_player(session=session, \r
506                     hashkey=player_events['P'], nick=nick)\r
507                 log.debug('Creating stats for %s' % player_events['P'])\r
508                 create_player_stats(session=session, player=player, game=game, \r
509                         player_events=player_events)\r
510     \r
511         session.commit()\r
512         log.debug('Success! Stats recorded.')\r
513         return Response('200 OK')\r
514     except Exception as e:\r
515         session.rollback()\r
516         raise e\r