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