]> de.git.xonotic.org Git - xonotic/xonstat.git/blob - xonstat/views/submission.py
88f1560177dc7bf10b3ff50511a752431a67c507
[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[:128]\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[:128]\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     # if the player is ranked #1 and it is a team game, set the game's winner\r
239     # to be the team of that player\r
240     # FIXME: this is a hack, should be using the 'W' field (not present)\r
241     if pgstat.rank == '1' and pgstat.team:\r
242         log.debug('Found rank 1. Logging.')\r
243         game.winner = pgstat.team\r
244         session.add(game)\r
245 \r
246     session.add(pgstat)\r
247     session.flush()\r
248 \r
249     return pgstat\r
250 \r
251 \r
252 def create_player_weapon_stats(session=None, player=None, \r
253         game=None, pgstat=None, player_events=None):\r
254     """\r
255     Creates accuracy records for each weapon used by a given player in a\r
256     given game. Parameters:\r
257 \r
258     session - SQLAlchemy session factory object\r
259     player - Player record who owns the weapon stats\r
260     game - Game record in which the stats were created\r
261     pgstat - Corresponding PlayerGameStat record for these weapon stats\r
262     player_events - dictionary containing the raw weapon values that need to be\r
263         transformed\r
264     """\r
265     pwstats = []\r
266 \r
267     for (key,value) in player_events.items():\r
268         matched = re.search("acc-(.*?)-cnt-fired", key)\r
269         if matched:\r
270             weapon_cd = matched.group(1)\r
271             pwstat = PlayerWeaponStat()\r
272             pwstat.player_id = player.player_id\r
273             pwstat.game_id = game.game_id\r
274             pwstat.player_game_stat_id = pgstat.player_game_stat_id\r
275             pwstat.weapon_cd = weapon_cd\r
276 \r
277             if 'n' in player_events:\r
278                 pwstat.nick = player_events['n']\r
279             else:\r
280                 pwstat.nick = player_events['P']\r
281 \r
282             if 'acc-' + weapon_cd + '-cnt-fired' in player_events:\r
283                 pwstat.fired = int(round(float(\r
284                         player_events['acc-' + weapon_cd + '-cnt-fired'])))\r
285             if 'acc-' + weapon_cd + '-fired' in player_events:\r
286                 pwstat.max = int(round(float(\r
287                         player_events['acc-' + weapon_cd + '-fired'])))\r
288             if 'acc-' + weapon_cd + '-cnt-hit' in player_events:\r
289                 pwstat.hit = int(round(float(\r
290                         player_events['acc-' + weapon_cd + '-cnt-hit'])))\r
291             if 'acc-' + weapon_cd + '-hit' in player_events:\r
292                 pwstat.actual = int(round(float(\r
293                         player_events['acc-' + weapon_cd + '-hit'])))\r
294             if 'acc-' + weapon_cd + '-frags' in player_events:\r
295                 pwstat.frags = int(round(float(\r
296                         player_events['acc-' + weapon_cd + '-frags'])))\r
297 \r
298             session.add(pwstat)\r
299             pwstats.append(pwstat)\r
300 \r
301     return pwstats\r
302 \r
303 \r
304 def parse_body(request):\r
305     """\r
306     Parses the POST request body for a stats submission\r
307     """\r
308     # storage vars for the request body\r
309     game_meta = {}\r
310     player_events = {}\r
311     current_team = None\r
312     players = []\r
313     \r
314     log.debug(request.body)\r
315 \r
316     for line in request.body.split('\n'):\r
317         try:\r
318             (key, value) = line.strip().split(' ', 1)\r
319     \r
320             if key in 'V' 'T' 'G' 'M' 'S' 'C' 'R' 'W':\r
321                 game_meta[key] = value\r
322 \r
323             if key == 'P':\r
324                 # if we were working on a player record already, append\r
325                 # it and work on a new one (only set team info)\r
326                 if len(player_events) != 0:\r
327                     players.append(player_events)\r
328                     player_events = {}\r
329     \r
330                 player_events[key] = value\r
331 \r
332             if key == 'e':\r
333                 (subkey, subvalue) = value.split(' ', 1)\r
334                 player_events[subkey] = subvalue\r
335             if key == 'n':\r
336                 player_events[key] = value\r
337             if key == 't':\r
338                 player_events[key] = value\r
339         except:\r
340             # no key/value pair - move on to the next line\r
341             pass\r
342     \r
343     # add the last player we were working on\r
344     if len(player_events) > 0:\r
345         players.append(player_events)\r
346 \r
347     return (game_meta, players)\r
348 \r
349 \r
350 def create_player_stats(session=None, player=None, game=None, \r
351         player_events=None):\r
352     """\r
353     Creates player game and weapon stats according to what type of player\r
354     """\r
355     pgstat = create_player_game_stat(session=session, \r
356         player=player, game=game, player_events=player_events)\r
357 \r
358     #TODO: put this into a config setting in the ini file?\r
359     if not re.search('^bot#\d+$', player_events['P']):\r
360         create_player_weapon_stats(session=session, \r
361             player=player, game=game, pgstat=pgstat,\r
362             player_events=player_events)\r
363     \r
364 \r
365 def stats_submit(request):\r
366     """\r
367     Entry handler for POST stats submissions.\r
368     """\r
369     try:\r
370         session = DBSession()\r
371 \r
372         (game_meta, players) = parse_body(request)  \r
373     \r
374         # verify required metadata is present\r
375         if 'T' not in game_meta or\\r
376             'G' not in game_meta or\\r
377             'M' not in game_meta or\\r
378             'S' not in game_meta:\r
379             log.debug("Required game meta fields (T, G, M, or S) missing. "\\r
380                     "Can't continue.")\r
381             raise Exception("Required game meta fields (T, G, M, or S) missing.")\r
382     \r
383         real_players = 0\r
384         for player_events in players:\r
385             if not player_events['P'].startswith('bot'):\r
386                 # removing 'joins' here due to bug, but it should be here\r
387                 if 'matches' in player_events\\r
388                     and 'scoreboardvalid' in player_events:\r
389                     real_players += 1\r
390 \r
391         #TODO: put this into a config setting in the ini file?\r
392         if real_players < 1:\r
393             raise Exception("The number of real players is below the minimum. "\\r
394                     "Stats will be ignored.")\r
395 \r
396         server = get_or_create_server(session=session, name=game_meta['S'])\r
397         gmap = get_or_create_map(session=session, name=game_meta['M'])\r
398 \r
399         if 'W' in game_meta:\r
400             winner = game_meta['W']\r
401         else:\r
402             winner = None\r
403 \r
404         game = create_game(session=session, \r
405                 start_dt=datetime.datetime(\r
406                     *time.gmtime(float(game_meta['T']))[:6]), \r
407                 server_id=server.server_id, game_type_cd=game_meta['G'], \r
408                 map_id=gmap.map_id, winner=winner)\r
409     \r
410         # find or create a record for each player\r
411         # and add stats for each if they were present at the end\r
412         # of the game\r
413         for player_events in players:\r
414             if 'n' in player_events:\r
415                 nick = player_events['n']\r
416             else:\r
417                 nick = None\r
418 \r
419             if 'matches' in player_events and 'scoreboardvalid' \\r
420                     in player_events:\r
421                 player = get_or_create_player(session=session, \r
422                     hashkey=player_events['P'], nick=nick)\r
423                 log.debug('Creating stats for %s' % player_events['P'])\r
424                 create_player_stats(session=session, player=player, game=game, \r
425                         player_events=player_events)\r
426     \r
427         session.commit()\r
428         log.debug('Success! Stats recorded.')\r
429         return Response('200 OK')\r
430     except Exception as e:\r
431         session.rollback()\r
432         raise e\r