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