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