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