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