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