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