]> de.git.xonotic.org Git - xonotic/xonstat.git/blob - xonstat/views.py
3769cec2181c42854530d1be3fa075608622491c
[xonotic/xonstat.git] / xonstat / views.py
1 import datetime
2 import time
3 import re
4 from pyramid.response import Response
5 from pyramid.view import view_config
6
7 from xonstat.models import *
8 from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound
9
10
11 import logging
12 log = logging.getLogger(__name__)
13
14 ##########################################################################
15 # This is the main index - the entry point to the entire site
16 ##########################################################################
17 @view_config(renderer='index.jinja2')
18 def main_index(request):
19     log.debug("testing logging; entered MainHandler.index()")
20     return {'project':'xonstat'}
21
22 ##########################################################################
23 # This is the player views area - only views pertaining to Xonotic players
24 # and their related information goes here
25 ##########################################################################
26 @view_config(renderer='player_index.mako')
27 def player_index(request):
28     players = DBSession.query(Player)
29     log.debug("testing logging; entered PlayerHandler.index()")
30     return {'players':players}
31
32 @view_config(renderer='player_info.mako')
33 def player_info(request):
34     player_id = request.matchdict['id']
35     try:
36         player = DBSession.query(Player).filter_by(player_id=player_id).one()
37         recent_games = DBSession.query("game_id", "server_id", "server_name", 
38                 "map_id", "map_name").\
39                 from_statement("select g.game_id, s.server_id, s.name as server_name, m.map_id, m.name as map_name "
40                         "from player_game_stats gs, games g, servers s, maps m "
41                         "where gs.player_id=:player_id "
42                         "and gs.game_id = g.game_id "
43                         "and g.server_id = s.server_id "
44                         "and g.map_id = m.map_id "
45                         "order by g.start_dt desc "
46                         "limit 10 offset 1").\
47                         params(player_id=player_id).all()
48
49         log.debug(recent_games)
50     except Exception as e:
51         player = None
52         recent_games = None
53     return {'player':player, 'recent_games':recent_games}
54
55
56 ##########################################################################
57 # This is the game views area - only views pertaining to Xonotic
58 # games and their related information goes here
59 ##########################################################################
60 def game_info(request):
61     game_id = request.matchdict['id']
62     try:
63         notfound = False
64
65         (start_dt, game_type_cd, server_id, server_name, map_id, map_name) = \
66         DBSession.query("start_dt", "game_type_cd", "server_id", 
67                 "server_name", "map_id", "map_name").\
68                 from_statement("select g.start_dt, g.game_type_cd, "
69                         "g.server_id, s.name as server_name, g.map_id, "
70                         "m.name as map_name "
71                         "from games g, servers s, maps m "
72                         "where g.game_id = :game_id "
73                         "and g.server_id = s.server_id "
74                         "and g.map_id = m.map_id").\
75                         params(game_id=game_id).one()
76
77         player_game_stats = DBSession.query(PlayerGameStat).\
78                 from_statement("select * from player_game_stats "
79                         "where game_id = :game_id "
80                         "order by score desc").\
81                             params(game_id=game_id).all()
82     except Exception as inst:
83         notfound = True
84         start_dt = None
85         game_type_cd = None
86         server_id = None
87         server_name = None
88         map_id = None
89         map_name = None
90         player_game_stats = None
91
92     return {'notfound':notfound,
93             'start_dt':start_dt,
94             'game_type_cd':game_type_cd,
95             'server_id':server_id,
96             'server_name':server_name,
97             'map_id':map_id,
98             'map_name':map_name,
99             'player_game_stats':player_game_stats}
100
101
102 ##########################################################################
103 # This is the server views area - only views pertaining to Xonotic
104 # servers and their related information goes here
105 ##########################################################################
106 def server_info(request):
107     server_id = request.matchdict['id']
108     try:
109         server = DBSession.query(Server).filter_by(server_id=server_id).one()
110         recent_games = DBSession.query("game_id", "server_id", "server_name", 
111                 "map_id", "map_name").\
112                 from_statement("select g.game_id, s.server_id, "
113                 "s.name as server_name, m.map_id, m.name as map_name "
114                 "from games g, servers s, maps m "
115                 "where g.server_id=:server_id "
116                 "and g.server_id = s.server_id "
117                 "and g.map_id = m.map_id "
118                 "order by g.start_dt desc "
119                 "limit 10 offset 1").\
120                  params(server_id=server_id).all()
121
122     except Exception as e:
123         server = None
124         recent_games = None
125     return {'server':server,
126             'recent_games':recent_games}
127
128
129 ##########################################################################
130 # This is the map views area - only views pertaining to Xonotic
131 # maps and their related information goes here
132 ##########################################################################
133 def map_info(request):
134     map_id = request.matchdict['id']
135     try:
136         gmap = DBSession.query(Map).filter_by(map_id=map_id).one()
137     except:
138         gmap = None
139     return {'gmap':gmap}
140
141
142 ##########################################################################
143 # This is the stats views area - only views pertaining to Xonotic
144 # statistics and its related information goes here
145 ##########################################################################
146 def get_or_create_server(session=None, name=None):
147     try:
148         # find one by that name, if it exists
149         server = session.query(Server).filter_by(name=name).one()
150         log.debug("Found server id {0} with name {1}.".format(
151             server.server_id, server.name))
152     except NoResultFound, e:
153         server = Server(name=name)
154         session.add(server)
155         session.flush()
156         log.debug("Created server id {0} with name {1}".format(
157             server.server_id, server.name))
158     except MultipleResultsFound, e:
159         # multiple found, so use the first one but warn
160         log.debug(e)
161         servers = session.query(Server).filter_by(name=name).order_by(
162                 Server.server_id).all()
163         server = servers[0]
164         log.debug("Created server id {0} with name {1} but found \
165                 multiple".format(
166             server.server_id, server.name))
167
168     return server
169
170 def get_or_create_map(session=None, name=None):
171     try:
172         # find one by the name, if it exists
173         gmap = session.query(Map).filter_by(name=name).one()
174         log.debug("Found map id {0} with name {1}.".format(gmap.map_id, 
175             gmap.name))
176     except NoResultFound, e:
177         gmap = Map(name=name)
178         session.add(gmap)
179         session.flush()
180         log.debug("Created map id {0} with name {1}.".format(gmap.map_id,
181             gmap.name))
182     except MultipleResultsFound, e:
183         # multiple found, so use the first one but warn
184         log.debug(e)
185         gmaps = session.query(Map).filter_by(name=name).order_by(
186                 Map.map_id).all()
187         gmap = gmaps[0]
188         log.debug("Found map id {0} with name {1} but found \
189                 multiple.".format(gmap.map_id, gmap.name))
190
191     return gmap
192
193 def create_game(session=None, start_dt=None, game_type_cd=None, 
194         server_id=None, map_id=None, winner=None):
195     game = Game(start_dt=start_dt, game_type_cd=game_type_cd,
196                 server_id=server_id, map_id=map_id, winner=winner)
197     session.add(game)
198     session.flush()
199     log.debug("Created game id {0} on server {1}, map {2} at time \
200             {3} and on map {4}".format(game.game_id, 
201                 server_id, map_id, start_dt, map_id))
202
203     return game
204
205 # search for a player and if found, create a new one (w/ hashkey)
206 def get_or_create_player(session=None, hashkey=None):
207     # if we have a bot
208     if re.search('^bot#\d+$', hashkey):
209         player = session.query(Player).filter_by(player_id=1).one()
210     # if we have an untracked player
211     elif re.search('^player#\d+$', hashkey):
212         player = session.query(Player).filter_by(player_id=2).one()
213     # else it is a tracked player
214     else:
215         # see if the player is already in the database
216         # if not, create one and the hashkey along with it
217         try:
218             hashkey = session.query(Hashkey).filter_by(
219                     hashkey=hashkey).one()
220             player = session.query(Player).filter_by(
221                     player_id=hashkey.player_id).one()
222             log.debug("Found existing player {0} with hashkey {1}.".format(
223                 player.player_id, hashkey.hashkey))
224         except:
225             player = Player()
226             session.add(player)
227             session.flush()
228             hashkey = Hashkey(player_id=player.player_id, hashkey=hashkey)
229             session.add(hashkey)
230             log.debug("Created player {0} with hashkey {1}.".format(
231                 player.player_id, hashkey.hashkey))
232
233     return player
234
235 def create_player_game_stat(session=None, player=None, 
236         game=None, player_events=None):
237
238     # in here setup default values (e.g. if game type is CTF then
239     # set kills=0, score=0, captures=0, pickups=0, fckills=0, etc
240     # TODO: use game's create date here instead of now()
241     pgstat = PlayerGameStat(create_dt=datetime.datetime.now())
242
243     # set player id from player record
244     pgstat.player_id = player.player_id
245
246     #set game id from game record
247     pgstat.game_id = game.game_id
248
249     # all games have a score
250     pgstat.score = 0
251
252     if game.game_type_cd == 'dm':
253         pgstat.kills = 0
254         pgstat.deaths = 0
255         pgstat.suicides = 0
256     elif game.game_type_cd == 'ctf':
257         pgstat.kills = 0
258         pgstat.captures = 0
259         pgstat.pickups = 0
260         pgstat.drops = 0
261         pgstat.returns = 0
262         pgstat.carrier_frags = 0
263
264     for (key,value) in player_events.items():
265         if key == 'n': pgstat.nick = value
266         if key == 't': pgstat.team = value
267         if key == 'rank': pgstat.rank = value
268         if key == 'alivetime': 
269             pgstat.alivetime = datetime.timedelta(seconds=int(round(float(value))))
270         if key == 'scoreboard-drops': pgstat.drops = value
271         if key == 'scoreboard-returns': pgstat.returns = value
272         if key == 'scoreboard-fckills': pgstat.carrier_frags = value
273         if key == 'scoreboard-pickups': pgstat.pickups = value
274         if key == 'scoreboard-caps': pgstat.captures = value
275         if key == 'scoreboard-score': pgstat.score = value
276         if key == 'scoreboard-deaths': pgstat.deaths = value
277         if key == 'scoreboard-kills': pgstat.kills = value
278         if key == 'scoreboard-suicides': pgstat.suicides = value
279
280     # check to see if we had a name, and if 
281     # not use the name from the player id
282     if pgstat.nick == None:
283         pgstat.nick = player.nick
284
285     session.add(pgstat)
286     session.flush()
287
288     return pgstat
289
290
291 def create_player_weapon_stats(session=None, player=None, 
292         game=None, player_events=None):
293     pwstats = []
294
295     for (key,value) in player_events.items():
296         matched = re.search("acc-(.*?)-cnt-fired", key)
297         if matched:
298             log.debug("Matched key: {0}".format(key))
299             weapon_cd = matched.group(1)
300             pwstat = PlayerWeaponStat()
301             pwstat.player_id = player.player_id
302             pwstat.game_id = game.game_id
303             pwstat.weapon_cd = weapon_cd
304
305             if 'n' in player_events:
306                 pwstat.nick = player_events['n']
307             else:
308                 pwstat.nick = player_events['P']
309
310             if 'acc-' + weapon_cd + '-cnt-fired' in player_events:
311                 pwstat.fired = int(round(float(
312                         player_events['acc-' + weapon_cd + '-cnt-fired'])))
313             if 'acc-' + weapon_cd + '-fired' in player_events:
314                 pwstat.max = int(round(float(
315                         player_events['acc-' + weapon_cd + '-fired'])))
316             if 'acc-' + weapon_cd + '-cnt-hit' in player_events:
317                 pwstat.hit = int(round(float(
318                         player_events['acc-' + weapon_cd + '-cnt-hit'])))
319             if 'acc-' + weapon_cd + '-hit' in player_events:
320                 pwstat.actual = int(round(float(
321                         player_events['acc-' + weapon_cd + '-hit'])))
322             if 'acc-' + weapon_cd + '-frags' in player_events:
323                 pwstat.frags = int(round(float(
324                         player_events['acc-' + weapon_cd + '-frags'])))
325
326             session.add(pwstat)
327             pwstats.append(pwstat)
328
329     return pwstats
330
331
332 def parse_body(request):
333     # storage vars for the request body
334     game_meta = {}
335     player_events = {}
336     current_team = None
337     players = []
338     
339     log.debug(request.body)
340
341     for line in request.body.split('\n'):
342         try:
343             (key, value) = line.strip().split(' ', 1)
344     
345             if key in 'V' 'T' 'G' 'M' 'S' 'C' 'R' 'W':
346                 game_meta[key] = value
347
348             if key == 't':
349                 current_team = value
350     
351             if key == 'P':
352                 # if we were working on a player record already, append
353                 # it and work on a new one (only set team info)
354                 if len(player_events) != 0:
355                     players.append(player_events)
356                     player_events = {'t':current_team}
357     
358                 player_events[key] = value
359     
360             if key == 'e':
361                 (subkey, subvalue) = value.split(' ', 1)
362                 player_events[subkey] = subvalue
363
364             if key == 'n':
365                 player_events[key] = value
366         except:
367             # no key/value pair - move on to the next line
368             pass
369     
370     # add the last player we were working on
371     if len(player_events) > 0:
372         players.append(player_events)
373
374     return (game_meta, players)
375
376
377 @view_config(renderer='stats_submit.mako')
378 def stats_submit(request):
379     try:
380         session = DBSession()
381
382         (game_meta, players) = parse_body(request)  
383     
384         # verify required metadata is present
385         if 'T' not in game_meta or\
386             'G' not in game_meta or\
387             'M' not in game_meta or\
388             'S' not in game_meta:
389             log.debug("Required game meta fields (T, G, M, or S) missing. "\
390                     "Can't continue.")
391             raise Exception("Required game meta fields (T, G, M, or S) missing.")
392     
393         has_real_players = False
394         for player_events in players:
395             if not player_events['P'].startswith('bot'):
396                 if 'joins' in player_events and 'matches' in player_events\
397                     and 'scoreboardvalid' in player_events:
398                     has_real_players = True
399
400         if not has_real_players:
401             raise Exception("No real players found. Stats ignored.")
402
403         server = get_or_create_server(session=session, name=game_meta['S'])
404         gmap = get_or_create_map(session=session, name=game_meta['M'])
405
406         if 'W' in game_meta:
407             winner = game_meta['W']
408         else:
409             winner = None
410
411         game = create_game(session=session, 
412                 start_dt=datetime.datetime(
413                     *time.gmtime(float(game_meta['T']))[:6]), 
414                 server_id=server.server_id, game_type_cd=game_meta['G'], 
415                 map_id=gmap.map_id, winner=winner)
416     
417         # find or create a record for each player
418         # and add stats for each if they were present at the end
419         # of the game
420         for player_events in players:
421             player = get_or_create_player(session=session, 
422                     hashkey=player_events['P'])
423             if 'joins' in player_events and 'matches' in player_events\
424                     and 'scoreboardvalid' in player_events:
425                 pgstat = create_player_game_stat(session=session, 
426                         player=player, game=game, player_events=player_events)
427                 if not player_events['P'].startswith('bot'):
428                     create_player_weapon_stats(session=session, 
429                             player=player, game=game, player_events=player_events)
430     
431         session.commit()
432         log.debug('Success! Stats recorded.')
433         return Response('200 OK')
434     except Exception as e:
435         session.rollback()
436         raise e