4 from colorsys import rgb_to_hls, hls_to_rgb
5 import sqlalchemy as sa
6 import sqlalchemy.sql.functions as func
7 from xonstat.models import *
8 from xonstat.util import qfont_decode, _all_colors
10 # similar to html_colors() from util.py
11 _contrast_threshold = 0.5
13 _dec_colors = [ (0.5,0.5,0.5),
26 def writepng(filename, buf, width, height):
27 width_byte_4 = width * 4
28 # fix color ordering (BGR -> RGB)
29 for byte in xrange(width*height):
31 buf[pos:pos+4] = buf[pos+2] + buf[pos+1] + buf[pos+0] + buf[pos+3]
33 #raw_data = b"".join(b'\x00' + buf[span:span + width_byte_4] for span in xrange((height - 1) * width * 4, -1, - width_byte_4))
34 raw_data = b"".join(b'\x00' + buf[span:span + width_byte_4] for span in range(0, (height-1) * width * 4 + 1, width_byte_4))
35 def png_pack(png_tag, data):
36 chunk_head = png_tag + data
37 return struct.pack("!I", len(data)) + chunk_head + struct.pack("!I", 0xFFFFFFFF & zlib.crc32(chunk_head))
40 png_pack(b'IHDR', struct.pack("!2I5B", width, height, 8, 6, 0, 0, 0)),
41 png_pack(b'IDAT', zlib.compress(raw_data, 9)),
42 png_pack(b'IEND', b'')])
43 f = open(filename, "wb")
52 # player data, will be filled by get_data()
58 def __getattr__(self, key):
59 if self.data.has_key(key):
63 def get_data(self, player_id):
64 """Return player data as dict.
66 This function is similar to the function in player.py but more optimized
72 # duel/dm/tdm/ctf elo + rank
74 player = DBSession.query(Player).filter(Player.player_id == player_id).one()
76 games_played = DBSession.query(
77 Game.game_type_cd, func.count(), func.sum(PlayerGameStat.alivetime)).\
78 filter(Game.game_id == PlayerGameStat.game_id).\
79 filter(PlayerGameStat.player_id == player_id).\
80 group_by(Game.game_type_cd).\
81 order_by(func.count().desc()).\
82 limit(3).all() # limit to 3 gametypes!
85 total_stats['games'] = 0
86 total_stats['games_breakdown'] = {} # this is a dictionary inside a dictionary .. dictception?
87 total_stats['games_alivetime'] = {}
88 total_stats['gametypes'] = []
89 for (game_type_cd, games, alivetime) in games_played:
90 total_stats['games'] += games
91 total_stats['gametypes'].append(game_type_cd)
92 total_stats['games_breakdown'][game_type_cd] = games
93 total_stats['games_alivetime'][game_type_cd] = alivetime
95 (total_stats['kills'], total_stats['deaths'], total_stats['alivetime'],) = DBSession.query(
96 func.sum(PlayerGameStat.kills),
97 func.sum(PlayerGameStat.deaths),
98 func.sum(PlayerGameStat.alivetime)).\
99 filter(PlayerGameStat.player_id == player_id).\
102 # (total_stats['wins'],) = DBSession.query(
104 # filter(Game.game_id == PlayerGameStat.game_id).\
105 # filter(PlayerGameStat.player_id == player_id).\
106 # filter(Game.winner == PlayerGameStat.team or PlayerGameStat.rank == 1).\
109 (total_stats['wins'],) = DBSession.\
110 query("total_wins").\
112 "select count(*) total_wins "
113 "from games g, player_game_stats pgs "
114 "where g.game_id = pgs.game_id "
115 "and player_id=:player_id "
116 "and (g.winner = pgs.team or pgs.rank = 1)"
117 ).params(player_id=player_id).one()
119 ranks = DBSession.query("game_type_cd", "rank", "max_rank").\
121 "select pr.game_type_cd, pr.rank, overall.max_rank "
122 "from player_ranks pr, "
123 "(select game_type_cd, max(rank) max_rank "
125 "group by game_type_cd) overall "
126 "where pr.game_type_cd = overall.game_type_cd "
127 "and player_id = :player_id "
129 params(player_id=player_id).all()
132 for gtc,rank,max_rank in ranks:
133 ranks_dict[gtc] = (rank, max_rank)
135 elos = DBSession.query(PlayerElo).\
136 filter_by(player_id=player_id).\
137 order_by(PlayerElo.elo.desc()).\
143 elos_dict[elo.game_type_cd] = elo.elo
147 'total_stats':total_stats,
156 # skin parameters, can be overriden by init
162 def __init__(self, name, **params):
166 'bg': "dark_wall", # None - plain; otherwise use given texture
167 'bgcolor': None, # transparent bg when bgcolor==None
168 'overlay': None, # add overlay graphic on top of bg
174 'nick_maxwidth': 330,
175 'gametype_fontsize':12,
176 'gametype_pos': (60,31),
177 'gametype_width': 110,
178 'gametype_height': 22,
179 'gametype_color': (1.0, 1.0, 1.0),
180 'gametype_text': "[ %s ]",
182 'nostats_fontsize': 12,
183 'nostats_pos': (60,59),
184 'nostats_color': (0.8, 0.2, 0.2),
185 'nostats_angle': -10,
186 'nostats_text': "no stats yet!",
189 'elo_color': (1.0, 1.0, 0.5),
190 'elo_text': "Elo %.0f",
193 'rank_color': (0.8, 0.8, 1.0),
194 'rank_text': "Rank %d of %d",
195 'wintext_fontsize': 10,
196 'wintext_pos': (508,3),
197 'wintext_width': 102,
198 'wintext_color': (0.8, 0.8, 0.8),
199 'wintext_bg': (0.8, 0.8, 0.8, 0.1),
200 'wintext_text': "Win Percentage",
202 'winp_pos': (508,19),
203 'winp_colortop': (0.2, 1.0, 1.0),
204 'winp_colorbot': (1.0, 1.0, 0.2),
206 'wins_pos': (508,33),
207 'wins_color': (0.6, 0.8, 0.8),
209 'loss_pos': (508,43),
210 'loss_color': (0.8, 0.8, 0.6),
211 'kdtext_fontsize': 10,
212 'kdtext_pos': (390,3),
214 'kdtext_color': (0.8, 0.8, 0.8),
215 'kdtext_bg': (0.8, 0.8, 0.8, 0.1),
216 'kdtext_text': "Kill Ratio",
219 'kdr_colortop': (0.2, 1.0, 0.2),
220 'kdr_colorbot': (1.0, 0.2, 0.2),
222 'kills_pos': (392,46),
223 'kills_color': (0.6, 0.8, 0.6),
224 'deaths_fontsize': 8,
225 'deaths_pos': (392,56),
226 'deaths_color': (0.8, 0.6, 0.6),
227 'ptime_fontsize': 10,
228 'ptime_pos': (451,60),
230 'ptime_bg': (0.8, 0.8, 0.8, 0.5),
231 'ptime_color': (0.1, 0.1, 0.1),
232 'ptime_text': "Playing Time: %s",
235 for k,v in params.items():
236 if self.params.has_key(k):
242 def __getattr__(self, key):
243 if self.params.has_key(key):
244 return self.params[key]
247 def render_image(self, data, output_filename):
248 """Render an image for the given player id."""
253 total_stats = data.total_stats
264 surf = C.ImageSurface(C.FORMAT_ARGB32, self.width, self.height)
265 ctx = C.Context(surf)
266 ctx.set_antialias(C.ANTIALIAS_GRAY)
270 if self.bgcolor != None:
271 # plain fillcolor, full transparency possible with (1,1,1,0)
273 ctx.set_operator(C.OPERATOR_SOURCE)
274 ctx.rectangle(0, 0, self.width, self.height)
275 ctx.set_source_rgba(self.bgcolor[0], self.bgcolor[1], self.bgcolor[2], self.bgcolor[3])
281 bg = C.ImageSurface.create_from_png("img/%s.png" % self.bg)
285 bg_w, bg_h = bg.get_width(), bg.get_height()
287 while bg_xoff < self.width:
289 while bg_yoff < self.height:
290 ctx.set_source_surface(bg, bg_xoff, bg_yoff)
291 #ctx.mask_surface(bg)
296 #print "Error: Can't load background texture: %s" % self.bg
299 # draw overlay graphic
300 if self.overlay != None:
302 overlay = C.ImageSurface.create_from_png("img/%s.png" % self.overlay)
303 ctx.set_source_surface(overlay, 0, 0)
304 #ctx.mask_surface(overlay)
307 #print "Error: Can't load overlay texture: %s" % self.overlay
311 ## draw player's nickname with fancy colors
313 # deocde nick, strip all weird-looking characters
314 qstr = qfont_decode(player.nick).replace('^^', '^').replace(u'\x00', '')
317 # replace weird characters that make problems - TODO
320 qstr = ''.join(chars)
321 stripped_nick = strip_colors(qstr.replace(' ', '_'))
323 # fontsize is reduced if width gets too large
324 ctx.select_font_face(font, C.FONT_SLANT_NORMAL, C.FONT_WEIGHT_NORMAL)
326 while shrinknick < 10:
327 ctx.set_font_size(self.nick_fontsize - shrinknick)
328 xoff, yoff, tw, th = ctx.text_extents(stripped_nick)[:4]
329 if tw > self.nick_maxwidth:
334 # determine width of single whitespace for later use
335 xoff, yoff, tw, th = ctx.text_extents("_")[:4]
338 # split nick into colored segments
340 _all_colors = re.compile(r'(\^\d|\^x[\dA-Fa-f]{3})')
341 parts = _all_colors.split(qstr)
342 while len(parts) > 0:
345 if _all_colors.match(txt):
346 tag = txt[1:] # strip leading '^'
353 if not txt or len(txt) == 0:
354 # only colorcode and no real text, skip this
358 if tag.startswith('x'):
359 r = int(tag[1] * 2, 16) / 255.0
360 g = int(tag[2] * 2, 16) / 255.0
361 b = int(tag[3] * 2, 16) / 255.0
362 hue, light, satur = rgb_to_hls(r, g, b)
363 if light < _contrast_threshold:
364 light = _contrast_threshold
365 r, g, b = hls_to_rgb(hue, light, satur)
367 r,g,b = _dec_colors[int(tag[0])]
369 r,g,b = _dec_colors[7]
371 ctx.set_source_rgb(r, g, b)
372 ctx.move_to(self.nick_pos[0] + xoffset, self.nick_pos[1])
375 xoff, yoff, tw, th = ctx.text_extents(txt)[:4]
376 tw += (len(txt)-len(txt.strip())) * space_w # account for lost whitespaces
379 ## print elos and ranks
381 # show up to three gametypes the player has participated in
383 for gt in total_stats['gametypes'][:self.num_gametypes]:
384 if self.gametype_pos:
385 ctx.select_font_face(font, C.FONT_SLANT_NORMAL, C.FONT_WEIGHT_BOLD)
386 ctx.set_font_size(self.gametype_fontsize)
387 ctx.set_source_rgb(self.gametype_color[0],self.gametype_color[1],self.gametype_color[2])
388 txt = self.gametype_text % gt.upper()
389 xoff, yoff, tw, th = ctx.text_extents(txt)[:4]
390 ctx.move_to(self.gametype_pos[0]+xoffset-xoff-tw/2, self.gametype_pos[1]-yoff)
393 # draw lines - TODO put this in overlay graphic
394 if self.overlay == None:
395 old_aa = ctx.get_antialias()
396 ctx.set_antialias(C.ANTIALIAS_NONE)
397 ctx.set_source_rgb(0.8, 0.8, 0.8)
398 ctx.set_line_width(1)
399 ctx.move_to(self.gametype_pos[0]+xoffset-self.gametype_width/2+5, self.gametype_pos[1]+14)
400 ctx.line_to(self.gametype_pos[0]+xoffset+self.gametype_width/2-5, self.gametype_pos[1]+14)
402 ctx.move_to(self.gametype_pos[0]+xoffset-self.gametype_width/2+5, self.gametype_pos[1]+self.gametype_height+14)
403 ctx.line_to(self.gametype_pos[0]+xoffset+self.gametype_width/2-5, self.gametype_pos[1]+self.gametype_height+14)
405 ctx.set_antialias(old_aa)
407 if not elos.has_key(gt) or not ranks.has_key(gt):
409 ctx.select_font_face(font, C.FONT_SLANT_NORMAL, C.FONT_WEIGHT_BOLD)
410 ctx.set_font_size(self.nostats_fontsize)
411 ctx.set_source_rgb(self.nostats_color[0],self.nostats_color[1],self.nostats_color[2])
412 txt = self.nostats_text
413 xoff, yoff, tw, th = ctx.text_extents(txt)[:4]
414 ctx.move_to(self.nostats_pos[0]+xoffset-xoff-tw/2, self.nostats_pos[1]-yoff)
416 ctx.rotate(math.radians(self.nostats_angle))
421 ctx.select_font_face(font, C.FONT_SLANT_NORMAL, C.FONT_WEIGHT_NORMAL)
422 ctx.set_font_size(self.elo_fontsize)
423 ctx.set_source_rgb(self.elo_color[0], self.elo_color[1], self.elo_color[2])
424 txt = self.elo_text % round(elos[gt], 0)
425 xoff, yoff, tw, th = ctx.text_extents(txt)[:4]
426 ctx.move_to(self.elo_pos[0]+xoffset-xoff-tw/2, self.elo_pos[1]-yoff)
429 ctx.select_font_face(font, C.FONT_SLANT_NORMAL, C.FONT_WEIGHT_NORMAL)
430 ctx.set_font_size(self.rank_fontsize)
431 ctx.set_source_rgb(self.rank_color[0], self.rank_color[1], self.rank_color[2])
432 txt = self.rank_text % ranks[gt]
433 xoff, yoff, tw, th = ctx.text_extents(txt)[:4]
434 ctx.move_to(self.rank_pos[0]+xoffset-xoff-tw/2, self.rank_pos[1]-yoff)
437 xoffset += self.gametype_width
440 # print win percentage
443 if self.overlay == None:
444 ctx.rectangle(self.wintext_pos[0]-self.wintext_width/2, self.wintext_pos[1]-self.wintext_fontsize/2+1,
445 self.wintext_width, self.wintext_fontsize+4)
446 ctx.set_source_rgba(self.wintext_bg[0], self.wintext_bg[1], self.wintext_bg[2], self.wintext_bg[3])
449 ctx.select_font_face(font, C.FONT_SLANT_NORMAL, C.FONT_WEIGHT_NORMAL)
450 ctx.set_font_size(self.wintext_fontsize)
451 ctx.set_source_rgb(self.wintext_color[0], self.wintext_color[1], self.wintext_color[2])
452 txt = self.wintext_text
453 xoff, yoff, tw, th = ctx.text_extents(txt)[:4]
454 ctx.move_to(self.wintext_pos[0]-xoff-tw/2, self.wintext_pos[1]-yoff)
457 total_games = total_stats['games']
458 wins, losses = total_stats['wins'], total_games-total_stats['wins']
461 ratio = float(wins)/total_games
462 txt = "%.2f%%" % round(ratio * 100, 2)
467 ctx.select_font_face(font, C.FONT_SLANT_NORMAL, C.FONT_WEIGHT_BOLD)
468 ctx.set_font_size(self.winp_fontsize)
469 r = ratio*self.winp_colortop[0] + (1-ratio)*self.winp_colorbot[0]
470 g = ratio*self.winp_colortop[1] + (1-ratio)*self.winp_colorbot[1]
471 b = ratio*self.winp_colortop[2] + (1-ratio)*self.winp_colorbot[2]
472 ctx.set_source_rgb(r, g, b)
473 xoff, yoff, tw, th = ctx.text_extents(txt)[:4]
474 ctx.move_to(self.winp_pos[0]-xoff-tw/2, self.winp_pos[1]-yoff)
478 ctx.select_font_face(font, C.FONT_SLANT_NORMAL, C.FONT_WEIGHT_NORMAL)
479 ctx.set_font_size(self.wins_fontsize)
480 ctx.set_source_rgb(self.wins_color[0], self.wins_color[1], self.wins_color[2])
481 txt = "%d win" % wins
484 xoff, yoff, tw, th = ctx.text_extents(txt)[:4]
485 ctx.move_to(self.wins_pos[0]-xoff-tw/2, self.wins_pos[1]-yoff)
489 ctx.set_font_size(self.loss_fontsize)
490 ctx.set_source_rgb(self.loss_color[0], self.loss_color[1], self.loss_color[2])
491 txt = "%d loss" % losses
494 xoff, yoff, tw, th = ctx.text_extents(txt)[:4]
495 ctx.move_to(self.loss_pos[0]-xoff-tw/2, self.loss_pos[1]-yoff)
499 # print kill/death ratio
502 if self.overlay == None:
503 ctx.rectangle(self.kdtext_pos[0]-self.kdtext_width/2, self.kdtext_pos[1]-self.kdtext_fontsize/2+1,
504 self.kdtext_width, self.kdtext_fontsize+4)
505 ctx.set_source_rgba(self.kdtext_bg[0], self.kdtext_bg[1], self.kdtext_bg[2], self.kdtext_bg[3])
508 ctx.select_font_face(font, C.FONT_SLANT_NORMAL, C.FONT_WEIGHT_NORMAL)
509 ctx.set_font_size(self.kdtext_fontsize)
510 ctx.set_source_rgb(self.kdtext_color[0], self.kdtext_color[1], self.kdtext_color[2])
511 txt = self.kdtext_text
512 xoff, yoff, tw, th = ctx.text_extents(txt)[:4]
513 ctx.move_to(self.kdtext_pos[0]-xoff-tw/2, self.kdtext_pos[1]-yoff)
516 kills, deaths = total_stats['kills'] , total_stats['deaths']
519 ratio = float(kills)/deaths
520 txt = "%.3f" % round(ratio, 3)
525 ctx.select_font_face(font, C.FONT_SLANT_NORMAL, C.FONT_WEIGHT_BOLD)
526 ctx.set_font_size(self.kdr_fontsize)
530 r = nr*self.kdr_colortop[0] + (1-nr)*self.kdr_colorbot[0]
531 g = nr*self.kdr_colortop[1] + (1-nr)*self.kdr_colorbot[1]
532 b = nr*self.kdr_colortop[2] + (1-nr)*self.kdr_colorbot[2]
533 ctx.set_source_rgb(r, g, b)
534 xoff, yoff, tw, th = ctx.text_extents(txt)[:4]
535 ctx.move_to(self.kdr_pos[0]-xoff-tw/2, self.kdr_pos[1]-yoff)
539 ctx.select_font_face(font, C.FONT_SLANT_NORMAL, C.FONT_WEIGHT_NORMAL)
540 ctx.set_font_size(self.kills_fontsize)
541 ctx.set_source_rgb(self.kills_color[0], self.kills_color[1], self.kills_color[2])
542 txt = "%d kill" % kills
545 xoff, yoff, tw, th = ctx.text_extents(txt)[:4]
546 ctx.move_to(self.kills_pos[0]-xoff-tw/2, self.kills_pos[1]+yoff)
550 ctx.select_font_face(font, C.FONT_SLANT_NORMAL, C.FONT_WEIGHT_NORMAL)
551 ctx.set_font_size(self.deaths_fontsize)
552 ctx.set_source_rgb(self.deaths_color[0], self.deaths_color[1], self.deaths_color[2])
553 if deaths is not None:
554 txt = "%d death" % deaths
559 xoff, yoff, tw, th = ctx.text_extents(txt)[:4]
560 ctx.move_to(self.deaths_pos[0]-xoff-tw/2, self.deaths_pos[1]+yoff)
567 if self.overlay == None:
568 ctx.rectangle( self.ptime_pos[0]-self.ptime_width/2, self.ptime_pos[1]-self.ptime_fontsize/2+1,
569 self.ptime_width, self.ptime_fontsize+4)
570 ctx.set_source_rgba(self.ptime_bg[0], self.ptime_bg[1], self.ptime_bg[2], self.ptime_bg[2])
573 ctx.select_font_face(font, C.FONT_SLANT_NORMAL, C.FONT_WEIGHT_NORMAL)
574 ctx.set_font_size(self.ptime_fontsize)
575 ctx.set_source_rgb(self.ptime_color[0], self.ptime_color[1], self.ptime_color[2])
576 txt = self.ptime_text % str(total_stats['alivetime'])
577 xoff, yoff, tw, th = ctx.text_extents(txt)[:4]
578 ctx.move_to(self.ptime_pos[0]-xoff-tw/2, self.ptime_pos[1]-yoff)
583 #surf.write_to_png(output_filename)
585 imgdata = surf.get_data()
586 writepng(output_filename, imgdata, self.width, self.height)