################################################################## # # XLRstats # statistics-generating plugin for B3 (www.bigbrotherbot.net) # (c) 2004, 2005 Tim ter Laak (ttlogic@xlr8or.com) # # This program is free software and licensed under the terms of # the GNU General Public License (GPL), version 2. # ################################################################## # CHANGELOG # 5/6/2008 - 0.6.0 - Mark Weirath (xlr8or@xlr8or.com) # Added weapon replacements # Added commands !xlrtopstats and !xlrhide # 8/9/2008 - 0.6.1 - Mark Weirath (xlr8or@xlr8or.com) # Added onemaponly for 24/7 servers to count rounds correct # 27/6/2009 - 0.6.5 - Mark Weirath (xlr8or@xlr8or.com) # No longer save worldkills # 28/6/2009 - 1.0.0 - Mark Weirath (xlr8or@xlr8or.com) # Added Action classes # 5/2/2010 - 2.0.0 - Mark Weirath (xlr8or@xlr8or.com) # Added Assist Bonus and History # 21/2/2010 - 2.1.0 - Mark Weirath (xlr8or@xlr8or.com) # Better assist mechanism # 23/2/2010 - 2.2.0 - Mark Weirath (xlr8or@xlr8or.com) # Adding table maintenance on startup # 24/2/2010 - 2.2.1 - Mark Weirath (xlr8or@xlr8or.com) # Repaired self._xlrstatstables bug # 24/2/2010 - 2.2.2 - Mark Weirath (xlr8or@xlr8or.com) # Repaired updateTableColumns() bug # 24-3-2010 - 2.2.3 - Mark Weirath - Minor fix in onEvent() # 10-8-2010 - 2.2.4 - Mark Weirath - BFBC2 adaptions (Bot Guid is Server, not WORLD) # 20-8-2010 - 2.2.5 - Mark Weirath # Allow external function call for cmd_xlrtopstats # Retrieve variables from webfront installation for topstats results # 23-8-2010 - 2.2.6 - Mark Weirath # BugFix: Requires ConfigFile for the commands # 3-9-2010 - 2.2.7 - Mark Weirath # Default action bonus set to +3 skillpoints (was 0) # 13-10-2010 - 2.2.8 - Mark Weirath # BugFix: Empty field webfront Url is now allowed in config # 08-11-2010 - 2.2.9 - Mark Weirath # Harden retrieval of webfront variables # 07-01-2011 - 2.3 - Mark Weirath # XLRstats can now install default database tables when missing # 07-01-2011 - 2.3.1 - Mark Weirath # Ability to disable plugin when not enough players are online # 07-01-2011 - 2.3.2 - Mark Weirath # Update weapon tables for cod7. # 16-04-2011 - 2.3.3 - Mark Weirath # Make sure we hide WORLD and Server in the webfront # 16-05-2011 - 2.4.0 - Mark Weirath # Make use of sql files for updating table, no more methods in the plugin # 15-07-2011 - 2.5.0 - Mark Weirath # Pythonized code. # Added ability to hide bots from webfront and exclude damage/kills to and from bots to be processed # 24-09-2011 - 2.6.0 - Mark Weirath # history functionality and crontabs moved to separate sub-plugin # 24-09-2011 - 2.6.1 - Mark Weirath # added update .sql file to distro to enable weaponnames > 32 characters. # no actual code was altered in the plugin except for the version to mark the change. # 19-10-2011 - 2.6.2 - Mark Weirath # moved polling for webfront variables to a separate thread, to avoid startup delay when website is offline # 31-01-2012 - 2.7.0 - Mark Weirath # integration of the ctime plugin as a subplugin # 03-03-2012 - 2.7.1 - Mark Weirath # added silent mode and some minor rewrites # CTime Plugin was created by Anubis and integrated in XLRstats since version 2.7.0 # Updates to this part of the plugin by: # AFC~Gagi2~ (gagi2@austrian-funclan.com) and xlr8or # This section is DoxuGen information. More information on how to comment your code # is available at http://wiki.bigbrotherbot.net/doku.php/customize:doxygen_rules ## @file # XLRstats Real Time playerstats plugin __author__ = 'Tim ter Laak / Mark Weirath' __version__ = '2.7.1' #final version before v3 # Version = major.minor.patches import datetime import time import re import thread import threading import urllib2 import b3 import b3.events import b3.plugin import b3.cron KILLER = "killer" VICTIM = "victim" ASSISTER = "assister" class XlrstatsPlugin(b3.plugin.Plugin): requiresConfigFile = True _world_clientid = None _ffa = ['dm', 'ffa', 'syc-ffa'] _damage_able_games = ['cod'] # will only count assists when damage is 50 points or more. _damage_ability = False hide_bots = True # set client.hide to True so bots are hidden from the stats exclude_bots = True # kills and damage to and from bots do not affect playerskill # history management _cronTabWeek = None _cronTabMonth = None _cronTabKillBonus = None # webfront variables webfrontUrl = '' webfrontConfigNr = 0 _minKills = 500 _minRounds = 50 _maxDays = 14 # config variables defaultskill = 1000 minlevel = 1 onemaponly = False Kfactor_high = 16 Kfactor_low = 4 Kswitch_kills = 100 steepness = 600 suicide_penalty_percent = 0.05 tk_penalty_percent = 0.1 kill_bonus = 1.5 assist_bonus = 0.5 assist_timespan = 2 # on non damage based games: damage before death timespan damage_assist_release = 10 # on damage based games: release the assist (wil overwrite self.assist_timespan on startup) prematch_maxtime = 70 announce = False keep_history = True keep_time = True minPlayers = 3 # minimum number of players to collect stats _currentNrPlayers = 0 # current number of players present silent = False # Disables the announcement when collecting stats = stealth mode # keep some private map data to detect prematches and restarts last_map = None last_roundtime = None # names for various stats tables playerstats_table = 'xlr_playerstats' weaponstats_table = 'xlr_weaponstats' weaponusage_table = 'xlr_weaponusage' bodyparts_table = 'xlr_bodyparts' playerbody_table = 'xlr_playerbody' opponents_table = 'xlr_opponents' mapstats_table = 'xlr_mapstats' playermaps_table = 'xlr_playermaps' actionstats_table = 'xlr_actionstats' playeractions_table = 'xlr_playeractions' clients_table = 'clients' penalties_table = 'penalties' # default tablenames for the history subplugin history_monthly_table = 'xlr_history_monthly' history_weekly_table = 'xlr_history_weekly' # default table name for the ctime subplugin ctime_table = 'ctime' # _defaultTableNames = True def startup(self): # get the admin plugin so we can register commands self._adminPlugin = self.console.getPlugin('admin') if not self._adminPlugin: # something is wrong, can't start without admin plugin self.error('Could not find admin plugin') return False # register our commands if 'commands' in self.config.sections(): for cmd in self.config.options('commands'): level = self.config.get('commands', cmd) sp = cmd.split('-') alias = None if len(sp) == 2: cmd, alias = sp func = self.getCmd(cmd) if func: self._adminPlugin.registerCommand(self, cmd, level, func, alias) #define a shortcut to the storage.query function self.query = self.console.storage.query # initialize tablenames PlayerStats._table = self.playerstats_table WeaponStats._table = self.weaponstats_table WeaponUsage._table = self.weaponusage_table Bodyparts._table = self.bodyparts_table PlayerBody._table = self.playerbody_table Opponents._table = self.opponents_table MapStats._table = self.mapstats_table PlayerMaps._table = self.playermaps_table ActionStats._table = self.actionstats_table PlayerActions._table = self.playeractions_table #--OBSOLETE # create tables if necessary # This needs to be done here, because table names were loaded from config #PlayerStats.createTable(ifNotExists=True) #WeaponStats.createTable(ifNotExists=True) #WeaponUsage.createTable(ifNotExists=True) #Bodyparts.createTable(ifNotExists=True) #PlayerBody.createTable(ifNotExists=True) #Opponents.createTable(ifNotExists=True) #MapStats.createTable(ifNotExists=True) #PlayerMaps.createTable(ifNotExists=True) #--end OBS # create default tables if not present # removed to avoid MySQL 5.5 issues locking the db #if self._defaultTableNames: # self.console.storage.queryFromFile("@b3/sql/xlrstats.sql", silent=True) # register the events we're interested in. self.registerEvent(b3.events.EVT_CLIENT_JOIN) self.registerEvent(b3.events.EVT_CLIENT_KILL) self.registerEvent(b3.events.EVT_CLIENT_KILL_TEAM) self.registerEvent(b3.events.EVT_CLIENT_SUICIDE) self.registerEvent(b3.events.EVT_GAME_ROUND_START) self.registerEvent(b3.events.EVT_CLIENT_ACTION) #for game-events/actions self.registerEvent(b3.events.EVT_CLIENT_DAMAGE) #for assist recognition # get the Client.id for the bot itself (guid: WORLD or Server(bfbc2/moh/hf)) sclient = self.console.clients.getByGUID("WORLD") if sclient is None: sclient = self.console.clients.getByGUID("Server") if sclient is not None: self._world_clientid = sclient.id self.debug('Got client id for B3: %s; %s' % (self._world_clientid, sclient.name)) #make sure its hidden in the webfront player = self.get_PlayerStats(sclient) if player: player.hide = 1 self.save_Stat(player) #determine the ability to work with damage based assists if self.console.gameName[:3] in self._damage_able_games: self._damage_ability = True self.assist_timespan = self.damage_assist_release #investigate if we can and want to keep a history self._xlrstatstables = [self.playerstats_table, self.weaponstats_table, self.weaponusage_table, self.bodyparts_table, self.playerbody_table, self.opponents_table, self.mapstats_table, self.playermaps_table, self.actionstats_table, self.playeractions_table] if self.keep_history: self._xlrstatstables = [self.playerstats_table, self.weaponstats_table, self.weaponusage_table, self.bodyparts_table, self.playerbody_table, self.opponents_table, self.mapstats_table, self.playermaps_table, self.actionstats_table, self.playeractions_table, self.history_monthly_table, self.history_weekly_table] _tables = self.showTables(xlrstats=True) if (self.history_monthly_table in _tables) and (self.history_monthly_table in _tables): self.verbose('History tables are present! Starting Subplugin XLRstatsHistory.') #start the xlrstats history plugin p = XlrstatshistoryPlugin(self.console, self.history_weekly_table, self.history_monthly_table, self.playerstats_table) p.startup() else: self.keep_history = False self._xlrstatstables = [self.playerstats_table, self.weaponstats_table, self.weaponusage_table, self.bodyparts_table, self.playerbody_table, self.opponents_table, self.mapstats_table, self.playermaps_table, self.actionstats_table, self.playeractions_table] self.error( 'History Tables are NOT present! Please run b3/docs/xlrstats.sql on your database to install missing tables!') #check and update columns in existing tables // This is not working with MySQL server 5.5! #self.updateTableColumns() #optimize xlrstats tables #self.optimizeTables(self._xlrstatstables) #let's try and get some variables from our webfront installation if self.webfrontUrl and self.webfrontUrl != '': thread1 = threading.Thread(target=self.getWebsiteVariables) thread1.start() else: self.debug('No Webfront Url available, using defaults') #set proper kill_bonus and crontab self.calculateKillBonus() if self._cronTabKillBonus: self.console.cron - self._cronTabKillBonus self._cronTabKillBonus = b3.cron.PluginCronTab(self, self.calculateKillBonus, 0, '*/10') self.console.cron + self._cronTabKillBonus #start the ctime subplugin if self.keep_time: p = CtimePlugin(self.console, self.ctime_table) p.startup() #start the xlrstats controller p = XlrstatscontrollerPlugin(self.console, self.minPlayers, self.silent) p.startup() #get the map we're in, in case this is a new map and we need to create a db record for it. map = self.get_MapStats(self.console.game.mapName) if map: self.verbose('Map %s ready' % map.name) msg = 'XLRstats v. %s by %s started.' % (__version__, __author__) self.console.say(msg) #end startup sequence def onLoadConfig(self): try: self.silent = self.config.getbool('settings', 'silent') except: self.debug('Using default value (%s) for settings::silent', self.silent) try: self.hide_bots = self.config.getbool('settings', 'hide_bots') except: self.debug('Using default value (%s) for settings::hide_bots', self.hide_bots) try: self.exclude_bots = self.config.getbool('settings', 'exclude_bots') except: self.debug('Using default value (%s) for settings::exclude_bots', self.exclude_bots) try: self.minPlayers = self.config.getint('settings', 'minplayers') except: self.debug('Using default value (%s) for settings::minplayers', self.minPlayers) try: self.webfrontUrl = self.config.get('settings', 'webfronturl') except: self.debug('Using default value (%s) for settings::webfronturl', self.webfrontUrl) try: self.webfrontConfigNr = self.config.getint('settings', 'servernumber') except: self.debug('Using default value (%i) for settings::servernumber', self.webfrontConfigNr) try: self.keep_history = self.config.getboolean('settings', 'keep_history') except: self.debug('Using default value (%i) for settings::keep_history', self.keep_history) try: self.onemaponly = self.config.getboolean('settings', 'onemaponly') except: self.debug('Using default value (%i) for settings::onemaponly', self.onemaponly) try: self.minlevel = self.config.getint('settings', 'minlevel') except: self.debug('Using default value (%i) for settings::minlevel', self.minlevel) try: self.defaultskill = self.config.getint('settings', 'defaultskill') except: self.debug('Using default value (%i) for settings::defaultskill', self.defaultskill) try: self.Kfactor_high = self.config.getint('settings', 'Kfactor_high') except: self.debug('Using default value (%i) for settings::Kfactor_high', self.Kfactor_high) try: self.Kfactor_low = self.config.getint('settings', 'Kfactor_low') except: self.debug('Using default value (%i) for settings::Kfactor_low', self.Kfactor_low) try: self.Kswitch_kills = self.config.getint('settings', 'Kswitch_kills') except: self.debug('Using default value (%i) for settings::Kswitch_kills', self.Kswitch_kills) try: self.steepness = self.config.getint('settings', 'steepness') except: self.debug('Using default value (%i) for settings::steepness', self.steepness) try: self.suicide_penalty_percent = self.config.getfloat('settings', 'suicide_penalty_percent') except: self.debug('Using default value (%f) for settings::suicide_penalty_percent', self.suicide_penalty_percent) try: self.tk_penalty_percent = self.config.getfloat('settings', 'tk_penalty_percent') except: self.debug('Using default value (%f) for settings::tk_penalty_percent', self.tk_penalty_percent) #--OBSOLETE #try: # self.kill_bonus = self.config.getfloat('settings', 'kill_bonus') #except: # self.kill_bonus = 1.2 # self.debug('Using default value (%f) for settings::kill_bonus', self.kill_bonus) #try: # self.assist_bonus = self.config.getfloat('settings', 'assist_bonus') # #cap off the assistbonus, so it will not be better rewarded than a kill # if self.assist_bonus > 0.9: # self.assist_bonus = 0.9 #except: # self.debug('Using default value (%f) for settings::assist_bonus', self.assist_bonus) #--end OBS try: self.assist_timespan = self.config.getint('settings', 'assist_timespan') except: self.debug('Using default value (%d) for settings::assist_timespan', self.assist_timespan) try: self.damage_assist_release = self.config.getint('settings', 'damage_assist_release') except: self.debug('Using default value (%d) for settings::damage_assist_release', self.damage_assist_release) try: self.prematch_maxtime = self.config.getint('settings', 'prematch_maxtime') except: self.debug('Using default value (%d) for settings::prematch_maxtime', self.prematch_maxtime) try: self.announce = self.config.getboolean('settings', 'announce') except: self.debug('Using default value (%d) for settings::announce', self.announce) try: self.keep_time = self.config.getboolean('settings', 'keep_time') except: self.debug('Using default value (%d) for settings::keep_time', self.keep_time) # Tablenames and stuff try: self.playerstats_table = self.config.get('tables', 'playerstats') self._defaultTableNames = False except: self.debug('Using default value (%s) for tables::playerstats', self.playerstats_table) try: self.weaponstats_table = self.config.get('tables', 'weaponstats') self._defaultTableNames = False except: self.debug('Using default value (%s) for tables::weaponstats', self.weaponstats_table) try: self.weaponusage_table = self.config.get('tables', 'weaponusage') self._defaultTableNames = False except: self.debug('Using default value (%s) for tables::weaponusage', self.weaponusage_table) try: self.bodyparts_table = self.config.get('tables', 'bodyparts') self._defaultTableNames = False except: self.debug('Using default value (%s) for tables::bodyparts', self.bodyparts_table) try: self.playerbody_table = self.config.get('tables', 'playerbody') self._defaultTableNames = False except: self.debug('Using default value (%s) for tables::playerbody', self.playerbody_table) try: self.opponents_table = self.config.get('tables', 'opponents') self._defaultTableNames = False except: self.debug('Using default value (%s) for tables::opponents', self.opponents_table) try: self.mapstats_table = self.config.get('tables', 'mapstats') self._defaultTableNames = False except: self.debug('Using default value (%s) for tables::mapstats', self.mapstats_table) try: self.playermaps_table = self.config.get('tables', 'playermaps') self._defaultTableNames = False except: self.debug('Using default value (%s) for tables::playermaps', self.playermaps_table) try: self.actionstats_table = self.config.get('tables', 'actionstats') self._defaultTableNames = False except: self.debug('Using default value (%s) for tables::actionstats', self.actionstats_table) try: self.playeractions_table = self.config.get('tables', 'playeractions') self._defaultTableNames = False except: self.debug('Using default value (%s) for tables::playeractions', self.playeractions_table) #history tables try: self.history_monthly_table = self.config.get('tables', 'history_monthly') self._defaultTableNames = False except: self.history_monthly_table = 'xlr_history_monthly' self.debug('Using default value (%s) for tables::history_monthly', self.history_monthly_table) try: self.history_weekly_table = self.config.get('tables', 'history_weekly') self._defaultTableNames = False except: self.history_weekly_table = 'xlr_history_weekly' self.debug('Using default value (%s) for tables::history_weekly', self.history_weekly_table) #ctime table try: self.ctime_table = self.config.get('tables', 'ctime') self._defaultTableNames = False except: self.ctime_table = 'ctime' self.debug('Using default value (%s) for tables::ctime', self.ctime_table) return def getCmd(self, cmd): cmd = 'cmd_%s' % cmd if hasattr(self, cmd): func = getattr(self, cmd) return func return None def onEvent(self, event): if event.type == b3.events.EVT_CLIENT_JOIN: self.join(event.client) elif event.type == b3.events.EVT_CLIENT_KILL: self.kill(event.client, event.target, event.data) elif event.type == b3.events.EVT_CLIENT_KILL_TEAM: if self.console.game.gameType in self._ffa: self.kill(event.client, event.target, event.data) else: self.teamkill(event.client, event.target, event.data) elif event.type == b3.events.EVT_CLIENT_DAMAGE: self.damage(event.client, event.target, event.data) elif event.type == b3.events.EVT_CLIENT_SUICIDE: self.suicide(event.client, event.target, event.data) elif event.type == b3.events.EVT_GAME_ROUND_START: self.roundstart() elif event.type == b3.events.EVT_CLIENT_ACTION: self.action(event.client, event.data) else: self.dumpEvent(event) def dumpEvent(self, event): self.debug('xlrstats.dumpEvent -- Type %s, Client %s, Target %s, Data %s', event.type, event.client, event.target, event.data) def getWebsiteVariables(self): """ Thread that polls for XLRstats webfront variables """ _request = str(self.webfrontUrl.rstrip('/')) + '/?config=' + str(self.webfrontConfigNr) + '&func=pluginreq' try: f = urllib2.urlopen(_request) _result = f.readline().split(',') # Our webfront will present us 3 values if len(_result) == 3: # Force the collected strings to their final type. If an error occurs they will fail the try statement. self._minKills = int(_result[0]) self._minRounds = int(_result[1]) self._maxDays = int(_result[2]) self.debug('Successfuly retrieved webfront variables: minkills: %i, minrounds: %i, maxdays: %i' % ( self._minKills, self._minRounds, self._maxDays)) except Exception: self.debug('Couldn\'t retrieve webfront variables, using defaults') def win_prob(self, player_skill, opponent_skill): return 1 / ( 10 ** ( (opponent_skill - player_skill) / self.steepness ) + 1 ) # Retrieves an existing stats record for given client, # or makes a new one IFF client's level is high enough # Otherwise (also on error), it returns None. def get_PlayerStats(self, client=None): if client is None: id = self._world_clientid else: id = client.id q = 'SELECT * from %s WHERE client_id = %s LIMIT 1' % (self.playerstats_table, id) cursor = self.query(q) if cursor and (cursor.rowcount > 0): r = cursor.getRow() s = PlayerStats() s.id = r['id'] s.client_id = r['client_id'] s.kills = r['kills'] s.deaths = r['deaths'] s.teamkills = r['teamkills'] s.teamdeaths = r['teamdeaths'] s.suicides = r['suicides'] s.ratio = r['ratio'] s.skill = r['skill'] s.assists = r['assists'] s.assistskill = r['assistskill'] s.curstreak = r['curstreak'] s.winstreak = r['winstreak'] s.losestreak = r['losestreak'] s.rounds = r['rounds'] s.hide = r['hide'] return s elif (client is None) or (client.maxLevel >= self.minlevel): s = PlayerStats() s._new = True s.skill = self.defaultskill s.client_id = id return s else: return None def get_PlayerAnon(self): return self.get_PlayerStats(None) def get_WeaponStats(self, name): s = WeaponStats() q = 'SELECT * from %s WHERE name = "%s" LIMIT 1' % (self.weaponstats_table, name) cursor = self.query(q) if cursor and (cursor.rowcount > 0): r = cursor.getRow() s.id = r['id'] s.name = r['name'] s.kills = r['kills'] s.suicides = r['suicides'] s.teamkills = r['teamkills'] return s else: s._new = True s.name = name return s def get_Bodypart(self, name): s = Bodyparts() q = 'SELECT * from %s WHERE name = "%s" LIMIT 1' % (self.bodyparts_table, name) cursor = self.query(q) if cursor and (cursor.rowcount > 0): r = cursor.getRow() s.id = r['id'] s.name = r['name'] s.kills = r['kills'] s.suicides = r['suicides'] s.teamkills = r['teamkills'] return s else: s._new = True s.name = name return s def get_MapStats(self, name): s = MapStats() q = 'SELECT * from %s WHERE name = "%s" LIMIT 1' % (self.mapstats_table, name) cursor = self.query(q) if cursor and (cursor.rowcount > 0): r = cursor.getRow() s.id = r['id'] s.name = r['name'] s.kills = r['kills'] s.suicides = r['suicides'] s.teamkills = r['teamkills'] s.rounds = r['rounds'] return s else: s._new = True s.name = name return s def get_WeaponUsage(self, weaponid, playerid): s = WeaponUsage() q = 'SELECT * from %s WHERE weapon_id = %s AND player_id = %s LIMIT 1' % ( self.weaponusage_table, weaponid, playerid) cursor = self.query(q) if cursor and (cursor.rowcount > 0): r = cursor.getRow() s.id = r['id'] s.player_id = r['player_id'] s.weapon_id = r['weapon_id'] s.kills = r['kills'] s.deaths = r['deaths'] s.suicides = r['suicides'] s.teamkills = r['teamkills'] s.teamdeaths = r['teamdeaths'] return s else: s._new = True s.player_id = playerid s.weapon_id = weaponid return s def get_Opponent(self, killerid, targetid): s = Opponents() q = 'SELECT * from %s WHERE killer_id = %s AND target_id = %s LIMIT 1' % ( self.opponents_table, killerid, targetid) cursor = self.query(q) if cursor and (cursor.rowcount > 0): r = cursor.getRow() s.id = r['id'] s.killer_id = r['killer_id'] s.target_id = r['target_id'] s.kills = r['kills'] s.retals = r['retals'] return s else: s._new = True s.killer_id = killerid s.target_id = targetid return s def get_PlayerBody(self, playerid, bodypartid): s = PlayerBody() q = 'SELECT * from %s WHERE bodypart_id = %s AND player_id = %s LIMIT 1' % ( self.playerbody_table, bodypartid, playerid) cursor = self.query(q) if cursor and (cursor.rowcount > 0): r = cursor.getRow() s.id = r['id'] s.player_id = r['player_id'] s.bodypart_id = r['bodypart_id'] s.kills = r['kills'] s.deaths = r['deaths'] s.suicides = r['suicides'] s.teamkills = r['teamkills'] s.teamdeaths = r['teamdeaths'] return s else: s._new = True s.player_id = playerid s.bodypart_id = bodypartid return s def get_PlayerMaps(self, playerid, mapid): if not mapid: self.error('Map not recognized, trying to initialise map...') map = self.get_MapStats(self.console.game.mapName) if map: self.verbose('Map %s successfully initialised.' % map.name) mapid = map.id else: return None s = PlayerMaps() q = 'SELECT * from %s WHERE map_id = %s AND player_id = %s LIMIT 1' % (self.playermaps_table, mapid, playerid) cursor = self.query(q) if cursor and (cursor.rowcount > 0): r = cursor.getRow() s.id = r['id'] s.player_id = r['player_id'] s.map_id = r['map_id'] s.kills = r['kills'] s.deaths = r['deaths'] s.suicides = r['suicides'] s.teamkills = r['teamkills'] s.teamdeaths = r['teamdeaths'] s.rounds = r['rounds'] return s else: s._new = True s.player_id = playerid s.map_id = mapid return s def get_ActionStats(self, name): s = ActionStats() q = 'SELECT * from %s WHERE name = "%s" LIMIT 1' % (self.actionstats_table, name) cursor = self.query(q) if cursor and (cursor.rowcount > 0): r = cursor.getRow() s.id = r['id'] s.name = r['name'] s.count = r['count'] return s else: s._new = True s.name = name return s def get_PlayerActions(self, playerid, actionid): s = PlayerActions() q = 'SELECT * from %s WHERE action_id = %s AND player_id = %s LIMIT 1' % ( self.playeractions_table, actionid, playerid) cursor = self.query(q) if cursor and (cursor.rowcount > 0): r = cursor.getRow() s.id = r['id'] s.player_id = r['player_id'] s.action_id = r['action_id'] s.count = r['count'] return s else: s._new = True s.player_id = playerid s.action_id = actionid return s def save_Stat(self, stat): #self.verbose('*----> XLRstats: Saving statistics for %s' %type(stat)) #self.verbose('*----> Contents: %s' %stat) if hasattr(stat, '_new'): q = stat._insertquery() #self.debug('Inserting using: ', q) cursor = self.query(q) if cursor.rowcount > 0: stat.id = cursor.lastrowid delattr(stat, '_new') else: q = stat._updatequery() #self.debug('Updating using: ', q) self.query(q) #print 'save_Stat: q= ', q #self.query(q) # we could not really do anything with error checking on saving. # If it fails, that's just bad luck. return def check_Assists(self, client, target, data, etype=None): #determine eventual assists // an assist only counts if damage was done within # secs. before death #it will also punish teammates that have a 'negative' assist! _count = 0 # number of assists to return _sum = 0 # sum of assistskill returned _vsum = 0 # sum of victims skill deduction returned self.verbose('----> XLRstats: %s Killed %s (%s), checking for assists' % (client.name, target.name, etype)) try: ainfo = target._attackers except: target._attackers = {} ainfo = target._attackers for k, v in ainfo.iteritems(): if k == client.cid: #don't award the killer for the assist aswell continue elif time.time() - v < self.assist_timespan: assister = self.console.clients.getByCID(k) self.verbose('----> XLRstats: assister = %s' % assister.name) anonymous = None victimstats = self.get_PlayerStats(target) assiststats = self.get_PlayerStats(assister) # if both should be anonymous, we have no work to do if (assiststats is None) and (victimstats is None): self.verbose('----> XLRstats: check_Assists: %s & %s both anonymous, continueing' % ( assister.name, target.name)) continue if victimstats is None: anonymous = VICTIM victimstats = self.get_PlayerAnon() if victimstats is None: continue if assiststats is None: anonymous = ASSISTER assiststats = self.get_PlayerAnon() if assiststats is None: continue #calculate the win probability for the assister and victim assist_prob = self.win_prob(assiststats.skill, victimstats.skill) #performance patch provided by IzNoGod: ELO states that assist_prob + victim_prob = 1 #victim_prob = self.win_prob(victimstats.skill, assiststats.skill) victim_prob = 1 - assist_prob self.verbose('----> XLRstats: win probability for %s: %s' % (assister.name, assist_prob)) self.verbose('----> XLRstats: win probability for %s: %s' % (target.name, victim_prob)) #get applicable weapon replacement actualweapon = data[1] for r in data: try: actualweapon = self.config.get('replacements', r) except: pass #get applicable weapon multiplier try: weapon_factor = self.config.getfloat('weapons', actualweapon) except: weapon_factor = 1.0 #calculate new skill for the assister if anonymous != ASSISTER: if assiststats.kills > self.Kswitch_kills: Kfactor = self.Kfactor_low else: Kfactor = self.Kfactor_high oldskill = assiststats.skill if ( target.team == assister.team ) and not ( self.console.game.gameType in self._ffa ): #assister is a teammate and needs skill and assists reduced _assistbonus = self.assist_bonus * Kfactor * weapon_factor * (0 - assist_prob) assiststats.skill = float(assiststats.skill) + _assistbonus assiststats.assistskill = float(assiststats.assistskill) + _assistbonus assiststats.assists -= 1 #negative assist self.verbose( '----> XLRstats: Assistpunishment deducted for %s: %s (oldsk: %.3f - newsk: %.3f)' % ( assister.name, assiststats.skill - oldskill, oldskill, assiststats.skill)) _count += 1 _sum += _assistbonus if self.announce and not assiststats.hide: assister.message('^5XLRstats:^7 Teamdamaged (%s) -> skill: ^1%.3f^7 -> ^2%.1f^7' % ( target.name, assiststats.skill - oldskill, assiststats.skill)) else: #this is a real assist _assistbonus = self.assist_bonus * Kfactor * weapon_factor * (1 - assist_prob) assiststats.skill = float(assiststats.skill) + _assistbonus assiststats.assistskill = float(assiststats.assistskill) + _assistbonus assiststats.assists += 1 self.verbose('----> XLRstats: Assistbonus awarded for %s: %s (oldsk: %.3f - newsk: %.3f)' % ( assister.name, assiststats.skill - oldskill, oldskill, assiststats.skill)) _count += 1 _sum += _assistbonus if self.announce and not assiststats.hide: assister.message('^5XLRstats:^7 Assistbonus (%s) -> skill: ^2+%.3f^7 -> ^2%.1f^7' % ( target.name, assiststats.skill - oldskill, assiststats.skill)) self.save_Stat(assiststats) #calculate new skill for the victim if anonymous != VICTIM: if victimstats.kills > self.Kswitch_kills: Kfactor = self.Kfactor_low else: Kfactor = self.Kfactor_high oldskill = victimstats.skill if ( target.team == assister.team ) and not ( self.console.game.gameType in self._ffa ): #assister was a teammate, this should not affect victims skill. pass else: #this is a real assist _assistdeduction = self.assist_bonus * Kfactor * weapon_factor * (0 - victim_prob) victimstats.skill = float(victimstats.skill) + _assistdeduction self.verbose('----> XLRstats: Assist skilldeduction for %s: %s (oldsk: %.3f - newsk: %.3f)' % ( target.name, victimstats.skill - oldskill, oldskill, victimstats.skill)) _vsum += _assistdeduction self.save_Stat(victimstats) #end of assist reward function, return the number of assists return _count, _sum, _vsum def kill(self, client, target, data): if (client is None) or (client.id == self._world_clientid): return if target is None: return if data is None: return # exclude botkills? if (client.bot or target.bot) and self.exclude_bots: self.verbose('Bot involved, do not process!') return _assists_count, _assists_sum, _victim_sum = self.check_Assists(client, target, data, 'kill') anonymous = None killerstats = self.get_PlayerStats(client) victimstats = self.get_PlayerStats(target) # if both should be anonymous, we have no work to do if (killerstats is None) and (victimstats is None): return if killerstats is None: anonymous = KILLER killerstats = self.get_PlayerAnon() if killerstats is None: return killerstats.skill = self.defaultskill if victimstats is None: anonymous = VICTIM victimstats = self.get_PlayerAnon() if victimstats is None: return #calculate winning probabilities for both players killer_prob = self.win_prob(killerstats.skill, victimstats.skill) #performance patch provided by IzNoGod: ELO states that killer_prob + victim_prob = 1 #victim_prob = self.win_prob(victimstats.skill, killerstats.skill) victim_prob = 1 - killer_prob #get applicable weapon replacement actualweapon = data[1] for r in data: try: actualweapon = self.config.get('replacements', r) except: pass #get applicable weapon multiplier try: weapon_factor = self.config.getfloat('weapons', actualweapon) except: weapon_factor = 1.0 #calculate new stats for the killer if anonymous != KILLER: if killerstats.kills > self.Kswitch_kills: Kfactor = self.Kfactor_low else: Kfactor = self.Kfactor_high oldskill = killerstats.skill #pure skilladdition for a 100% kill _skilladdition = self.kill_bonus * Kfactor * weapon_factor * (1 - killer_prob) #deduct the assists from the killers skill, but no more than 50% if _assists_sum == 0: pass elif _assists_sum >= ( _skilladdition / 2 ): _skilladdition /= 2 self.verbose( '----> XLRstats: Killer: assists > 50perc: %.3f - skilladd: %.3f' % (_assists_sum, _skilladdition)) else: _skilladdition -= _assists_sum self.verbose( '----> XLRstats: Killer: assists < 50perc: %.3f - skilladd: %.3f' % (_assists_sum, _skilladdition)) killerstats.skill = float(killerstats.skill) + _skilladdition self.verbose('----> XLRstats: Killer: oldsk: %.3f - newsk: %.3f' % (oldskill, killerstats.skill)) killerstats.kills = int(killerstats.kills) + 1 if int(killerstats.deaths) != 0: killerstats.ratio = float(killerstats.kills) / float(killerstats.deaths) else: killerstats.ratio = 0.0 if int(killerstats.curstreak) > 0: killerstats.curstreak = int(killerstats.curstreak) + 1 else: killerstats.curstreak = 1 if int(killerstats.curstreak) > int(killerstats.winstreak): killerstats.winstreak = int(killerstats.curstreak) else: killerstats.winstreak = int(killerstats.winstreak) if self.announce and not killerstats.hide: client.message('^5XLRstats:^7 Killed %s -> skill: ^2+%.3f^7 -> ^2%.1f^7' % ( target.name, (killerstats.skill - oldskill), killerstats.skill)) self.save_Stat(killerstats) #calculate new stats for the victim if anonymous != VICTIM: if victimstats.kills > self.Kswitch_kills: Kfactor = self.Kfactor_low else: Kfactor = self.Kfactor_high oldskill = victimstats.skill #pure skilldeduction for a 100% kill _skilldeduction = Kfactor * weapon_factor * (0 - victim_prob) #deduct the assists from the victims skill deduction, but no more than 50% if _victim_sum == 0: pass elif _victim_sum <= ( _skilldeduction / 2 ): #carefull, negative numbers here _skilldeduction /= 2 self.verbose('----> XLRstats: Victim: assists > 50perc: %.3f - skilldeduct: %.3f' % ( _victim_sum, _skilldeduction)) else: _skilldeduction -= _victim_sum self.verbose('----> XLRstats: Victim: assists < 50perc: %.3f - skilldeduct: %.3f' % ( _victim_sum, _skilldeduction)) victimstats.skill = float(victimstats.skill) + _skilldeduction self.verbose('----> XLRstats: Victim: oldsk: %.3f - newsk: %.3f' % (oldskill, victimstats.skill)) victimstats.deaths = int(victimstats.deaths) + 1 victimstats.ratio = float(victimstats.kills) / float(victimstats.deaths) if int(victimstats.curstreak) < 0: victimstats.curstreak = int(victimstats.curstreak) - 1 else: victimstats.curstreak = -1 if victimstats.curstreak < int(victimstats.losestreak): victimstats.losestreak = victimstats.curstreak else: victimstats.losestreak = int(victimstats.losestreak) if self.announce and not victimstats.hide: target.message('^5XLRstats:^7 Killed by %s -> skill: ^1%.3f^7 -> ^2%.1f^7' % ( client.name, (victimstats.skill - oldskill), victimstats.skill)) self.save_Stat(victimstats) #make sure the record for anonymous is really created with an insert once if anonymous: if (anonymous == KILLER) and (hasattr(killerstats, '_new')): self.save_Stat(killerstats) elif (anonymous == VICTIM) and (hasattr(victimstats, '_new')): self.save_Stat(victimstats) #adjust the "opponents" table to register who killed who opponent = self.get_Opponent(targetid=victimstats.id, killerid=killerstats.id) retal = self.get_Opponent(targetid=killerstats.id, killerid=victimstats.id) #the above should always succeed, but you never know... if opponent and retal: opponent.kills += 1 retal.retals += 1 self.save_Stat(opponent) self.save_Stat(retal) #adjust weapon statistics weaponstats = self.get_WeaponStats(name=actualweapon) if weaponstats: weaponstats.kills += 1 self.save_Stat(weaponstats) w_usage_killer = self.get_WeaponUsage(playerid=killerstats.id, weaponid=weaponstats.id) w_usage_victim = self.get_WeaponUsage(playerid=victimstats.id, weaponid=weaponstats.id) if w_usage_killer and w_usage_victim: w_usage_killer.kills += 1 w_usage_victim.deaths += 1 self.save_Stat(w_usage_killer) self.save_Stat(w_usage_victim) #adjust bodypart statistics bodypart = self.get_Bodypart(name=data[2]) if bodypart: bodypart.kills += 1 self.save_Stat(bodypart) bp_killer = self.get_PlayerBody(playerid=killerstats.id, bodypartid=bodypart.id) bp_victim = self.get_PlayerBody(playerid=victimstats.id, bodypartid=bodypart.id) if bp_killer and bp_victim: bp_killer.kills += 1 bp_victim.deaths += 1 self.save_Stat(bp_killer) self.save_Stat(bp_victim) #adjust map statistics map = self.get_MapStats(self.console.game.mapName) if map: map.kills += 1 self.save_Stat(map) map_killer = self.get_PlayerMaps(playerid=killerstats.id, mapid=map.id) map_victim = self.get_PlayerMaps(playerid=victimstats.id, mapid=map.id) if map_killer and map_victim: map_killer.kills += 1 map_victim.deaths += 1 self.save_Stat(map_killer) self.save_Stat(map_victim) #end of kill function return def damage(self, client, target, data): if client.id == self._world_clientid: self.verbose('----> XLRstats: onDamage: WORLD-damage, moving on...') return None if client.cid == target.cid: self.verbose( '----> XLRstats: onDamage: self damage: %s damaged %s, continueing' % (client.name, target.name)) return None # exclude botdamage? if (client.bot or target.bot) and self.exclude_bots: self.verbose('Bot involved, do not process!') return None #check if game is _damage_able -> 50 points or more damage will award an assist if self._damage_ability and data[0] < 50: self.verbose('---> XLRstats: Not enough damage done to award an assist') return try: target._attackers[client.cid] = time.time() except: target._attackers = {} target._attackers[client.cid] = time.time() self.verbose('----> XLRstats: onDamage: attacker added: %s (%s) damaged %s (%s)' % ( client.name, client.cid, target.name, target.cid)) self.verbose('----> XLRstats: Assistinfo: %s' % target._attackers) def suicide(self, client, target, data): if client is None: return if target is None: return if data is None: return self.check_Assists(client, target, data, 'suicide') playerstats = self.get_PlayerStats(client) if playerstats is None: #anonymous player. We're not interested :) return playerstats.suicides += 1 if playerstats.curstreak < 0: playerstats.curstreak -= 1 else: playerstats.curstreak = -1 if playerstats.curstreak < playerstats.losestreak: playerstats.losestreak = playerstats.curstreak oldskill = playerstats.skill playerstats.skill = (1 - (self.suicide_penalty_percent / 100.0) ) * float(playerstats.skill) if self.announce and not playerstats.hide: client.message('^5XLRstats:^7 Suicide -> skill: ^1%.3f^7 -> ^2%.1f^7' % ( playerstats.skill - oldskill, playerstats.skill)) self.save_Stat(playerstats) #get applicable weapon replacement actualweapon = data[1] for r in data: try: actualweapon = self.config.get('replacements', r) except: pass #update weapon stats weaponstats = self.get_WeaponStats(name=actualweapon) if weaponstats: weaponstats.suicides += 1 self.save_Stat(weaponstats) w_usage = self.get_WeaponUsage(playerid=playerstats.id, weaponid=weaponstats.id) if w_usage: w_usage.suicides += 1 self.save_Stat(w_usage) #update bodypart stats bodypart = self.get_Bodypart(name=data[2]) if bodypart: bodypart.suicides += 1 self.save_Stat(bodypart) bp_player = self.get_PlayerBody(playerid=playerstats.id, bodypartid=bodypart.id) if bp_player: bp_player.suicides = int(bp_player.suicides) + 1 self.save_Stat(bp_player) #adjust map statistics map = self.get_MapStats(self.console.game.mapName) if map: map.suicides += 1 self.save_Stat(map) map_player = self.get_PlayerMaps(playerid=playerstats.id, mapid=map.id) if map_player: map_player.suicides += 1 self.save_Stat(map_player) #end of function suicide return def teamkill(self, client, target, data): if client is None: return if target is None: return if data is None: return anonymous = None self.check_Assists(client, target, data, 'teamkill') killerstats = self.get_PlayerStats(client) victimstats = self.get_PlayerStats(target) # if both should be anonymous, we have no work to do if (killerstats is None) and (victimstats is None): return if killerstats is None: anonymous = KILLER killerstats = self.get_PlayerAnon() if killerstats is None: return killerstats.skill = self.defaultskill if victimstats is None: anonymous = VICTIM victimstats = self.get_PlayerAnon() if victimstats is None: return victimstats.skill = self.defaultskill if anonymous != KILLER: #Calculate new stats for the killer oldskill = killerstats.skill killerstats.skill = (1 - (self.tk_penalty_percent / 100.0) ) * float(killerstats.skill) killerstats.teamkills += 1 killerstats.curstreak = 0 # break off current streak as it is now "impure" if self.announce and not killerstats.hide: client.message('^5XLRstats:^7 Teamkill -> skill: ^1%.3f^7 -> ^2%.1f^7' % ( killerstats.skill - oldskill, killerstats.skill)) self.save_Stat(killerstats) if anonymous != VICTIM: #Calculate new stats for the victim victimstats.teamdeaths += 1 self.save_Stat(victimstats) # do not register a teamkill in the "opponents" table #get applicable weapon replacement actualweapon = data[1] for r in data: try: actualweapon = self.config.get('replacements', r) except: pass #adjust weapon statistics weaponstats = self.get_WeaponStats(name=actualweapon) if weaponstats: weaponstats.teamkills += 1 self.save_Stat(weaponstats) w_usage_killer = self.get_WeaponUsage(playerid=killerstats.id, weaponid=weaponstats.id) w_usage_victim = self.get_WeaponUsage(playerid=victimstats.id, weaponid=weaponstats.id) if w_usage_killer and w_usage_victim: w_usage_killer.teamkills += 1 w_usage_victim.teamdeaths += 1 self.save_Stat(w_usage_killer) self.save_Stat(w_usage_victim) #adjust bodypart statistics bodypart = self.get_Bodypart(name=data[2]) if bodypart: bodypart.teamkills += 1 self.save_Stat(bodypart) bp_killer = self.get_PlayerBody(playerid=killerstats.id, bodypartid=bodypart.id) bp_victim = self.get_PlayerBody(playerid=victimstats.id, bodypartid=bodypart.id) if bp_killer and bp_victim: bp_killer.teamkills += 1 bp_victim.teamdeaths += 1 self.save_Stat(bp_killer) self.save_Stat(bp_victim) #adjust map statistics map = self.get_MapStats(self.console.game.mapName) if map: map.teamkills += 1 self.save_Stat(map) map_killer = self.get_PlayerMaps(playerid=killerstats.id, mapid=map.id) map_victim = self.get_PlayerMaps(playerid=victimstats.id, mapid=map.id) if map_killer and map_victim: map_killer.teamkills += 1 map_victim.teamdeaths += 1 self.save_Stat(map_killer) self.save_Stat(map_victim) #end of function teamkill return def join(self, client): if client is None: return # test if it is a bot and flag it if client.guid[:3] == 'BOT': self.verbose('Bot found!') client.bot = True player = self.get_PlayerStats(client) if player: player.rounds = int(player.rounds) + 1 if client.bot: if self.hide_bots: self.verbose('Hiding Bot!') player.hide = True else: self.verbose('Unhiding Bot!') player.hide = False self.save_Stat(player) map = self.get_MapStats(self.console.game.mapName) if map: playermap = self.get_PlayerMaps(player.id, map.id) if playermap: playermap.rounds += 1 self.save_Stat(playermap) return def roundstart(self): #disable k/d counting if minimum players are not met if self.last_map is None: self.last_map = self.console.game.mapName #self.last_roundtime = self.console.game._roundTimeStart else: if not self.onemaponly and ( self.last_map == self.console.game.mapName) and ( self.console.game.roundTime() < self.prematch_maxtime): #( self.console.game._roundTimeStart - self.last_roundtime < self.prematch_maxtime) ): return else: self.last_map = self.console.game.mapName #self.last_roundtime = self.console.game._roundTimeStart map = self.get_MapStats(self.console.game.mapName) if map: map.rounds += 1 self.save_Stat(map) return def action(self, client, data): #self.verbose('----> XLRstats: Entering actionfunc.') if client is None: return action = self.get_ActionStats(name=data) if action: action.count += 1 #self.verbose('----> XLRstats: Actioncount: %s' %action.count) #self.verbose('----> XLRstats: Actionname: %s' %action.name) #if hasattr(action, '_new'): # self.verbose('----> XLRstats: insertquery: %s' %action._insertquery()) #else: # self.verbose('----> XLRstats: updatequery: %s' %action._updatequery()) self.save_Stat(action) #is it an anonymous client, stop here playerstats = self.get_PlayerStats(client) if playerstats is None: #self.verbose('----> XLRstats: Anonymous client') return playeractions = self.get_PlayerActions(playerid=playerstats.id, actionid=action.id) if playeractions: playeractions.count += 1 #self.verbose('----> XLRstats: Players Actioncount: %s' %playeractions.count) #if hasattr(playeractions, '_new'): # self.verbose('----> XLRstats: insertquery: %s' %playeractions._insertquery()) #else: # self.verbose('----> XLRstats: updatequery: %s' %playeractions._updatequery()) self.save_Stat(playeractions) #get applicable action bonus try: action_bonus = self.config.getfloat('actions', action.name) #self.verbose('----> XLRstats: Found a bonus for %s: %s' %(action.name, action_bonus)) except: action_bonus = 3 if action_bonus: #self.verbose('----> XLRstats: Old Skill: %s.' %playerstats.skill) playerstats.skill += action_bonus #self.verbose('----> XLRstats: New Skill: %s.' %playerstats.skill) self.save_Stat(playerstats) return def cmd_xlrstats(self, data, client, cmd=None): """\ [<name>] - list a players XLR stats """ if data: sclient = self._adminPlugin.findClientPrompt(data, client) if not sclient: return else: sclient = client stats = self.get_PlayerStats(sclient) if stats: if stats.hide == 1: client.message('^3XLR Stats: ^7Stats for %s are not available (hidden).' % sclient.exactName) return None else: message = '^3XLR Stats: ^7%s ^7: K ^2%s ^7D ^3%s ^7TK ^1%s ^7Ratio ^5%1.02f ^7Skill ^3%1.02f' % ( sclient.exactName, stats.kills, stats.deaths, stats.teamkills, stats.ratio, stats.skill) cmd.sayLoudOrPM(client, message) else: client.message('^3XLR Stats: ^7Could not find stats for %s' % sclient.exactName) return # Start a thread to get the top players def cmd_xlrtopstats(self, data, client, cmd=None, ext=False): """\ [<#>] - list the top # players of the last 14 days. """ thread.start_new_thread(self.doTopList, (data, client, cmd, ext)) return # Retrieves the Top # Players def doTopList(self, data, client, cmd=None, ext=False): if data: if re.match('^[0-9]+$', data, re.I): limit = int(data) if limit > 10: limit = 10 else: limit = 3 #q = 'SELECT `%s`.name, `%s`.time_edit, `%s`.id, kills, deaths, ratio, skill, winstreak, losestreak, rounds, fixed_name, ip FROM `%s`, `%s` WHERE (`%s`.id = `%s`.client_id) AND ((`%s`.kills > 100) OR (`%s`.rounds > 10)) AND (`%s`.hide = 0) AND (UNIX_TIMESTAMP(NOW()) - `%s`.time_edit < 14*60*60*24) AND `%s`.id NOT IN ( SELECT distinct(target.id) FROM `%s` as penalties, `%s` as target WHERE (penalties.type = "Ban" OR penalties.type = "TempBan") AND inactive = 0 AND penalties.client_id = target.id AND ( penalties.time_expire = -1 OR penalties.time_expire > UNIX_TIMESTAMP(NOW()) ) ) ORDER BY `%s`.`skill` DESC LIMIT %s' % (self.clients_table, self.clients_table, self.playerstats_table, self.clients_table, self.playerstats_table, self.clients_table, self.playerstats_table, self.playerstats_table, self.playerstats_table, self.playerstats_table, self.clients_table, self.clients_table, self.penalties_table, self.clients_table, self.playerstats_table, limit) q = 'SELECT `%s`.name, `%s`.time_edit, `%s`.id, kills, deaths, ratio, skill, winstreak, losestreak, rounds, fixed_name, ip \ FROM `%s`, `%s` \ WHERE (`%s`.id = `%s`.client_id) \ AND ((`%s`.kills > %s) \ OR (`%s`.rounds > %s)) \ AND (`%s`.hide = 0) \ AND (UNIX_TIMESTAMP(NOW()) - `%s`.time_edit < %s*60*60*24) \ AND `%s`.id NOT IN \ ( SELECT distinct(target.id) FROM `%s` as penalties, `%s` as target \ WHERE (penalties.type = "Ban" \ OR penalties.type = "TempBan") \ AND inactive = 0 \ AND penalties.client_id = target.id \ AND ( penalties.time_expire = -1 \ OR penalties.time_expire > UNIX_TIMESTAMP(NOW()) ) ) \ ORDER BY `%s`.`skill` DESC LIMIT %s'\ % (self.clients_table, self.clients_table, self.playerstats_table, self.clients_table, self.playerstats_table,\ self.clients_table, self.playerstats_table, self.playerstats_table, self._minKills, self.playerstats_table,\ self._minRounds, self.playerstats_table, self.clients_table, self._maxDays, self.clients_table, self.penalties_table,\ self.clients_table, self.playerstats_table, limit) cursor = self.query(q) if cursor and (cursor.rowcount > 0): message = '^3XLR Stats Top %s Players:' % limit if ext: self.console.say(message) else: cmd.sayLoudOrPM(client, message) c = 1 while not cursor.EOF: r = cursor.getRow() message = '^3# %s: ^7%s ^7: Skill ^3%1.02f ^7Ratio ^5%1.02f ^7Kills: ^2%s' % ( c, r['name'], r['skill'], r['ratio'], r['kills']) if ext: self.console.say(message) else: cmd.sayLoudOrPM(client, message) cursor.moveNext() c += 1 time.sleep(1) else: self.debug('No players qualified for the toplist yet...') message = 'Qualify for the toplist by making %i kills, or playing %i rounds!' % ( self._minKills, self._minRounds) if ext: self.console.say(message) else: cmd.sayLoudOrPM(client, message) return None return def cmd_xlrhide(self, data, client, cmd=None): """\ <player> <on/off> - Hide/unhide a player from the stats """ # this will split the player name and the message input = self._adminPlugin.parseUserCmd(data) if input: # input[0] is the player id sclient = self._adminPlugin.findClientPrompt(input[0], client) if not sclient: # a player matchin the name was not found, a list of closest matches will be displayed # we can exit here and the user will retry with a more specific player return False else: client.message('^7Invalid data, try !help xlrhide') return False if not input[1]: client.message('^7Missing data, try !help xlrhide') return False m = input[1] if m in ('on', '1'): if client != sclient: sclient.message('^3You are invisible in xlrstats!') client.message('^3%s INVISIBLE in xlrstats!' % sclient.exactName) hide = 1 elif m in ('off', '0'): if client != sclient: sclient.message('^3You are visible in xlrstats!') client.message('^3%s VISIBLE in xlrstats!' % sclient.exactName) hide = 0 else: client.message('^7Invalid or missing data, try !help xlrhide') player = self.get_PlayerStats(sclient) if player: player.hide = int(hide) self.save_Stat(player) return ## @todo: add mysql condition def updateTableColumns(self): self.verbose('Checking if we need to update tables for version 2.0.0') #v2.0.0 additions to the playerstats table: self._addTableColumn('assists', PlayerStats._table, 'MEDIUMINT( 8 ) NOT NULL DEFAULT "0" AFTER `skill`') self._addTableColumn('assistskill', PlayerStats._table, 'FLOAT NOT NULL DEFAULT "0" AFTER `assists`') #alterations to columns in existing tables: self._updateTableColumns() return None #end of update check def _addTableColumn(self, c1, t1, specs): try: self.query('SELECT `%s` FROM %s limit 1;' % (c1, t1)) except Exception, e: if e[0] == 1054: self.console.debug('Column does not yet exist: %s' % e) self.query('ALTER TABLE %s ADD `%s` %s ;' % (t1, c1, specs)) self.console.info('Created new column `%s` on %s' % (c1, t1)) else: self.console.error('Query failed - %s: %s' % (type(e), e)) def _updateTableColumns(self): try: #need to update the weapon-identifier columns in these tables for cod7. This game knows over 255 weapons/variations self.query( 'ALTER TABLE `%s` CHANGE `id` `id` SMALLINT( 5 ) UNSIGNED NOT NULL AUTO_INCREMENT;' % WeaponStats._table) self.query( 'ALTER TABLE `%s` CHANGE `weapon_id` `weapon_id` SMALLINT( 5 ) UNSIGNED NOT NULL DEFAULT "0";' % WeaponUsage._table) except: pass def showTables(self, xlrstats=False): _tables = [] q = 'SHOW TABLES' cursor = self.query(q) if cursor and (cursor.rowcount > 0): while not cursor.EOF: r = cursor.getRow() n = str(r.values()[0]) if xlrstats and not n in self._xlrstatstables: pass else: _tables.append(r.values()[0]) cursor.moveNext() if xlrstats: self.console.verbose('Available XLRstats tables in this database: %s' % _tables) else: self.console.verbose('Available tables in this database: %s' % _tables) return _tables def optimizeTables(self, t=None): if not t: t = self.showTables() if type(t) == type(''): _tables = str(t) else: _tables = ', '.join(t) self.debug('Optimizing Table(s): %s' % _tables) try: self.query('OPTIMIZE TABLE %s' % _tables) self.debug('Optimize Success') except Exception, msg: self.error('Optimizing Table(s) Failed: %s, trying to repair...' % msg) self.repairTables(t) def repairTables(self, t=None): if not t: t = self.showTables() if type(t) == type(''): _tables = str(t) else: _tables = ', '.join(t) self.debug('Repairing Table(s): %s' % _tables) try: self.query('REPAIR TABLE %s' % _tables) self.debug('Repair Success') except Exception, msg: self.error('Repairing Table(s) Failed: %s' % msg) def calculateKillBonus(self): self.debug('Calculating kill_bonus') # make sure max and diff are floating numbers (may be redundant) max = 0.0 diff = 0.0 _oldkillbonus = self.kill_bonus q = 'SELECT MAX(skill) AS max_skill FROM %s' % self.playerstats_table cursor = self.query(q) r = cursor.getRow() max = r['max_skill'] if max is None: max = self.defaultskill diff = max - self.defaultskill if diff < 0: self.kill_bonus = 2.0 elif diff < 400: self.kill_bonus = 1.5 else: c = 200.0 / diff + 1 self.kill_bonus = round(c, 1) self.assist_bonus = self.kill_bonus / 3 if self.kill_bonus != _oldkillbonus: self.debug('kill_bonus set to: %s' % self.kill_bonus) self.debug('assist_bonus set to: %s' % self.assist_bonus) class XlrstatscontrollerPlugin(b3.plugin.Plugin): """This is a helper class/plugin that enables and disables the main XLRstats plugin It can not be called directly or separately from the XLRstats plugin!""" def __init__(self, console, minPlayers=3, silent=False): self.console = console self.minPlayers = minPlayers self.silent = silent # empty message cache self._messages = {} self.registerEvent(b3.events.EVT_STOP) self.registerEvent(b3.events.EVT_EXIT) def startup(self): self.console.debug('Starting SubPlugin: XlrstatsControllerPlugin') #get a reference to the main Xlrstats plugin self._xlrstatsPlugin = self.console.getPlugin('xlrstats') # register the events we're interested in. self.registerEvent(b3.events.EVT_CLIENT_JOIN) self.registerEvent(b3.events.EVT_GAME_ROUND_START) def onEvent(self, event): if event.type == b3.events.EVT_CLIENT_JOIN: self.checkMinPlayers() elif event.type == b3.events.EVT_GAME_ROUND_START: self.checkMinPlayers(_roundstart=True) def checkMinPlayers(self, _roundstart=False): """Checks if minimum amount of players are present if minimum amount of players is reached will enable stats collecting and if not it disables stats counting on next roundstart""" self._currentNrPlayers = len(self.console.clients.getList()) self.debug( 'Checking number of players online. Minimum = %s, Current = %s' % (self.minPlayers, self._currentNrPlayers)) if self._currentNrPlayers < self.minPlayers and self._xlrstatsPlugin.isEnabled() and _roundstart: self.info('Disabling XLRstats: Not enough players online') if not self.silent: self.console.say('XLRstats Disabled: Not enough players online!') self._xlrstatsPlugin.disable() elif self._currentNrPlayers >= self.minPlayers and not self._xlrstatsPlugin.isEnabled(): self.info('Enabling XLRstats: Collecting Stats') if not self.silent: self.console.say('XLRstats Enabled: Now collecting stats!') self._xlrstatsPlugin.enable() else: if self._xlrstatsPlugin.isEnabled(): _status = 'Enabled' else: _status = 'Disabled' self.debug('Nothing to do at the moment. XLRstats is already %s' % _status) class XlrstatshistoryPlugin(b3.plugin.Plugin): """This is a helper class/plugin that saves history snapshots It can not be called directly or separately from the XLRstats plugin!""" def __init__(self, console, weeklyTable, monthlyTable, playerstatsTable): self.console = console self.history_weekly_table = weeklyTable self.history_monthly_table = monthlyTable self.playerstats_table = playerstatsTable # empty message cache self._messages = {} self.registerEvent(b3.events.EVT_STOP) self.registerEvent(b3.events.EVT_EXIT) def startup(self): self.console.debug('Starting SubPlugin: XlrstatsHistoryPlugin') #define a shortcut to the storage.query function self.query = self.console.storage.query self.console.verbose('XlrstatshistoryPlugin: Installing history Crontabs:') # remove existing crontabs try: self.console.cron - self._cronTabMonth except: pass try: self.console.cron - self._cronTabWeek except: pass try: # install crontabs self._cronTabMonth = b3.cron.PluginCronTab(self, self.snapshot_month, 0, 0, 0, 1, '*', '*') self.console.cron + self._cronTabMonth self._cronTabWeek = b3.cron.PluginCronTab(self, self.snapshot_week, 0, 0, 0, '*', '*', 1) # day 1 is monday self.console.cron + self._cronTabWeek except Exception, msg: self.console.error('XlrstatshistoryPlugin: Unable to install History Crontabs: %s' % msg) def snapshot_month(self): sql = ( 'INSERT INTO ' + self.history_monthly_table + ' (`client_id` , `kills` , `deaths` , `teamkills` , `teamdeaths` , `suicides` ' + ', `ratio` , `skill` , `assists` , `assistskill` , `winstreak` , `losestreak` , `rounds`, `year`, `month`, `week`, `day`)' + ' SELECT `client_id` , `kills`, `deaths` , `teamkills` , `teamdeaths` , `suicides` , `ratio` , `skill` , `assists` , `assistskill` , `winstreak` ' + ', `losestreak` , `rounds`, YEAR(NOW()), MONTH(NOW()), WEEK(NOW(),3), DAY(NOW())' + ' FROM `' + self.playerstats_table + '`' ) try: self.query(sql) self.verbose('Monthly XLRstats snapshot created') except Exception, msg: self.error('Creating history snapshot failed: %s' % msg) def snapshot_week(self): sql = ( 'INSERT INTO ' + self.history_weekly_table + ' (`client_id` , `kills` , `deaths` , `teamkills` , `teamdeaths` , `suicides` ' + ', `ratio` , `skill` , `assists` , `assistskill` , `winstreak` , `losestreak` , `rounds`, `year`, `month`, `week`, `day`)' + ' SELECT `client_id` , `kills`, `deaths` , `teamkills` , `teamdeaths` , `suicides` , `ratio` , `skill` , `assists` , `assistskill` , `winstreak` ' + ', `losestreak` , `rounds`, YEAR(NOW()), MONTH(NOW()), WEEK(NOW(),3), DAY(NOW())' + ' FROM `' + self.playerstats_table + '`' ) try: self.query(sql) self.verbose('Weekly XLRstats snapshot created') except Exception, msg: self.error('Creating history snapshot failed: %s' % msg) class TimeStats: came = None left = None client = None class CtimePlugin(b3.plugin.Plugin): """This is a helper class/plugin that saves client join and disconnect time info It can not be called directly or separately from the XLRstats plugin!""" _clients = {} _cronTab = None _max_age_in_days = 31 _hours = 5 _minutes = 0 def __init__(self, console, cTimeTable): self.console = console self.ctime_table = cTimeTable self.registerEvent(b3.events.EVT_CLIENT_AUTH) self.registerEvent(b3.events.EVT_CLIENT_DISCONNECT) self.query = self.console.storage.query tzName = self.console.config.get('b3', 'time_zone').upper() tzOffest = b3.timezones.timezones[tzName] hoursGMT = (self._hours - tzOffest)%24 self.debug(u'%02d:%02d %s => %02d:%02d UTC' % (self._hours, self._minutes, tzName, hoursGMT, self._minutes)) self.info(u'everyday at %2d:%2d %s, connection info older than %s days will be deleted' % (self._hours, self._minutes, tzName, self._max_age_in_days)) self._cronTab = b3.cron.PluginCronTab(self, self.purge, 0, self._minutes, hoursGMT, '*', '*', '*') self.console.cron + self._cronTab def purge(self): if not self._max_age_in_days or self._max_age_in_days == 0: self.warning(u'max_age is invalid [%s]' % self._max_age_in_days) return False self.info(u'purge of connection info older than %s days ...' % self._max_age_in_days) q = "DELETE FROM %s WHERE came < %i" % (self.ctime_table, (self.console.time() - (self._max_age_in_days*24*60*60))) self.debug(u'CTIME QUERY: %s ' % q) cursor = self.console.storage.query(q) def onEvent(self, event): if event.type == b3.events.EVT_CLIENT_AUTH: if not event.client or\ not event.client.id or\ event.client.cid == None or\ not event.client.connected or\ event.client.hide: return self.update_time_stats_connected(event.client) elif event.type == b3.events.EVT_CLIENT_DISCONNECT: self.update_time_stats_exit(event.data) def update_time_stats_connected(self, client): if self._clients.has_key(client.cid): self.debug(u'CTIME CONNECTED: Client exist! : %s' % client.cid) tmpts = self._clients[client.cid] if tmpts.client.guid == client.guid: self.debug(u'CTIME RECONNECTED: Player %s connected again, but playing since: %s' % (client.exactName, tmpts.came)) return else: del self._clients[client.cid] ts = TimeStats() ts.client = client ts.came = datetime.datetime.now() self._clients[client.cid] = ts self.debug(u'CTIME CONNECTED: Player %s started playing at: %s' % (client.exactName, ts.came)) def formatTD(self, td): hours = td // 3600 minutes = (td % 3600) // 60 seconds = td % 60 return '%s:%s:%s' % (hours, minutes, seconds) def update_time_stats_exit(self, clientid): self.debug(u'CTIME LEFT:') if self._clients.has_key(clientid): ts = self._clients[clientid] # Fail: Sometimes PB in cod4 returns 31 character guids, we need to dump them. Lets look ahead and do this for the whole codseries. #if(self.console.gameName[:3] == 'cod' and self.console.PunkBuster and len(ts.client.guid) != 32): # pass #else: ts.left = datetime.datetime.now() diff = (int(time.mktime(ts.left.timetuple())) - int(time.mktime(ts.came.timetuple()))) self.debug(u'CTIME LEFT: Player: %s played this time: %s sec' % (ts.client.exactName, diff)) self.debug(u'CTIME LEFT: Player: %s played this time: %s' % (ts.client.exactName, self.formatTD(diff))) #INSERT INTO `ctime` (`guid`, `came`, `left`) VALUES ("6fcc4f6d9d8eb8d8457fd72d38bb1ed2", 1198187868, 1226081506) q = 'INSERT INTO %s (guid, came, gone, nick) VALUES (\"%s\", \"%s\", \"%s\", \"%s\")' % (self.ctime_table, ts.client.guid, int(time.mktime(ts.came.timetuple())), int(time.mktime(ts.left.timetuple())), ts.client.name) self.query(q) self._clients[clientid].left = None self._clients[clientid].came = None self._clients[clientid].client = None del self._clients[clientid] else: self.debug(u'CTIME LEFT: Player %s var not set!' % clientid) #----------------------------------------------------------------------------------------------------------------------- # This is an abstract class. Do not call directly. class StatObject(object): _table = None def _insertquery(self): return None def _updatequery(self): return None class PlayerStats(StatObject): #default name of the table for this data object _table = 'playerstats' #fields of the table id = None client_id = 0 kills = 0 deaths = 0 teamkills = 0 teamdeaths = 0 suicides = 0 ratio = 0 skill = 0 assists = 0 assistskill = 0 curstreak = 0 winstreak = 0 losestreak = 0 rounds = 0 hide = 0 # the following fields are used only by the PHP presentation code fixed_name = "" def _insertquery(self): q = 'INSERT INTO %s ( client_id, kills, deaths, teamkills, teamdeaths, suicides, ratio, skill, assists, assistskill, curstreak, winstreak, losestreak, rounds, hide ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)' % ( self._table, self.client_id, self.kills, self.deaths, self.teamkills, self.teamdeaths, self.suicides, self.ratio , self.skill, self.assists, self.assistskill, self.curstreak, self.winstreak, self.losestreak, self.rounds, self.hide) return q def _updatequery(self): q = 'UPDATE %s SET client_id=%s, kills=%s, deaths=%s, teamkills=%s, teamdeaths=%s, suicides=%s, ratio=%s, skill=%s, assists=%s, assistskill=%s, curstreak=%s, winstreak=%s, losestreak=%s, rounds=%s, hide=%s WHERE id=%s' % ( self._table, self.client_id, self.kills, self.deaths, self.teamkills, self.teamdeaths, self.suicides, self.ratio , self.skill, self.assists, self.assistskill, self.curstreak, self.winstreak, self.losestreak, self.rounds, self.hide, self.id) return q class WeaponStats(StatObject): #default name of the table for this data object _table = 'weaponstats' #fields of the table id = None name = '' kills = 0 suicides = 0 teamkills = 0 def _insertquery(self): q = 'INSERT INTO %s ( name, kills, suicides, teamkills ) VALUES ("%s", %s, %s, %s)' % ( self._table, self.name, self.kills, self.suicides, self.teamkills) return q def _updatequery(self): q = 'UPDATE %s SET name="%s", kills=%s, suicides=%s, teamkills=%s WHERE id=%s' % ( self._table, self.name, self.kills, self.suicides, self.teamkills, self.id) return q class WeaponUsage(StatObject): #default name of the table for this data object _table = 'weaponusage' #fields of the table id = None player_id = 0 weapon_id = 0 kills = 0 deaths = 0 suicides = 0 teamkills = 0 teamdeaths = 0 def _insertquery(self): q = 'INSERT INTO %s ( player_id, weapon_id, kills, deaths, suicides, teamkills, teamdeaths ) VALUES (%s, %s, %s, %s, %s, %s, %s)' % ( self._table, self.player_id, self.weapon_id, self.kills, self.deaths, self.suicides, self.teamkills, self.teamdeaths) return q def _updatequery(self): q = 'UPDATE %s SET player_id=%s, weapon_id=%s, kills=%s, deaths=%s, suicides=%s, teamkills=%s, teamdeaths=%s WHERE id=%s' % ( self._table, self.player_id, self.weapon_id, self.kills, self.deaths, self.suicides, self.teamkills, self.teamdeaths, self.id) return q class Bodyparts(StatObject): #default name of the table for this data object _table = 'bodyparts' #fields of the table id = None name = '' kills = 0 suicides = 0 teamkills = 0 def _insertquery(self): q = 'INSERT INTO %s ( name, kills, suicides, teamkills ) VALUES ("%s", %s, %s, %s)' % ( self._table, self.name, self.kills, self.suicides, self.teamkills) return q def _updatequery(self): q = 'UPDATE %s SET name="%s", kills=%s, suicides=%s, teamkills=%s WHERE id=%s' % ( self._table, self.name, self.kills, self.suicides, self.teamkills, self.id) return q class MapStats(StatObject): #default name of the table for this data object _table = 'mapstats' #fields of the table id = None name = '' kills = 0 suicides = 0 teamkills = 0 rounds = 0 def _insertquery(self): q = 'INSERT INTO %s ( name, kills, suicides, teamkills, rounds ) VALUES ("%s", %s, %s, %s, %s)' % ( self._table, self.name, self.kills, self.suicides, self.teamkills, self.rounds) return q def _updatequery(self): q = 'UPDATE %s SET name="%s", kills=%s, suicides=%s, teamkills=%s, rounds=%s WHERE id=%s' % ( self._table, self.name, self.kills, self.suicides, self.teamkills, self.rounds, self.id) return q class PlayerBody(StatObject): #default name of the table for this data object _table = 'playerbody' #fields of the table id = None player_id = 0 bodypart_id = 0 kills = 0 deaths = 0 suicides = 0 teamkills = 0 teamdeaths = 0 def _insertquery(self): q = 'INSERT INTO %s ( player_id, bodypart_id, kills, deaths, suicides, teamkills, teamdeaths ) VALUES (%s, %s, %s, %s, %s, %s, %s)' % ( self._table, self.player_id, self.bodypart_id, self.kills, self.deaths, self.suicides, self.teamkills, self.teamdeaths) return q def _updatequery(self): q = 'UPDATE %s SET player_id=%s, bodypart_id=%s, kills=%s, deaths=%s, suicides=%s, teamkills=%s, teamdeaths=%s WHERE id=%s' % ( self._table, self.player_id, self.bodypart_id, self.kills, self.deaths, self.suicides, self.teamkills, self.teamdeaths, self.id) return q class PlayerMaps(StatObject): #default name of the table for this data object _table = 'playermaps' #fields of the table id = 0 player_id = 0 map_id = 0 kills = 0 deaths = 0 suicides = 0 teamkills = 0 teamdeaths = 0 rounds = 0 def _insertquery(self): q = 'INSERT INTO %s ( player_id, map_id, kills, deaths, suicides, teamkills, teamdeaths, rounds ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s)' % ( self._table, self.player_id, self.map_id, self.kills, self.deaths, self.suicides, self.teamkills, self.teamdeaths, self.rounds) return q def _updatequery(self): q = 'UPDATE %s SET player_id=%s, map_id=%s, kills=%s, deaths=%s, suicides=%s, teamkills=%s, teamdeaths=%s, rounds=%s WHERE id=%s' % ( self._table, self.player_id, self.map_id, self.kills, self.deaths, self.suicides, self.teamkills, self.teamdeaths, self.rounds, self.id) return q class Opponents(StatObject): #default name of the table for this data object _table = 'opponents' #fields of the table id = None killer_id = 0 target_id = 0 kills = 0 retals = 0 def _insertquery(self): q = 'INSERT INTO %s (killer_id, target_id, kills, retals) VALUES (%s, %s, %s, %s)' % ( self._table, self.killer_id, self.target_id, self.kills, self.retals) return q def _updatequery(self): q = 'UPDATE %s SET killer_id=%s, target_id=%s, kills=%s, retals=%s WHERE id=%s' % ( self._table, self.killer_id, self.target_id, self.kills, self.retals, self.id) return q class ActionStats(StatObject): #default name of the table for this data object _table = 'actionstats' #fields of the table id = None name = '' count = 0 def _insertquery(self): q = 'INSERT INTO %s (name, count) VALUES ("%s", %s)' % (self._table, self.name, self.count) return q def _updatequery(self): q = 'UPDATE %s SET name="%s", count=%s WHERE id=%s' % (self._table, self.name, self.count, self.id) return q class PlayerActions(StatObject): #default name of the table for this data object _table = 'playeractions' #fields of the table id = None player_id = 0 action_id = 0 count = 0 def _insertquery(self): q = 'INSERT INTO %s ( player_id, action_id, count ) VALUES (%s, %s, %s)' % ( self._table, self.player_id, self.action_id, self.count) return q def _updatequery(self): q = 'UPDATE %s SET player_id=%s, action_id=%s, count=%s WHERE id=%s' % ( self._table, self.player_id, self.action_id, self.count, self.id) return q if __name__ == '__main__': print '\nThis is version ' + __version__ + ' by ' + __author__ + ' for BigBrotherBot.\n' """\ Crontab: * * * * * command to be executed - - - - - | | | | | | | | | +----- day of week (0 - 6) (Sunday=0) | | | +------- month (1 - 12) | | +--------- day of month (1 - 31) | +----------- hour (0 - 23) +------------- min (0 - 59) Query: INSERT INTO xlr_history_weekly (`client_id` , `kills` , `deaths` , `teamkills` , `teamdeaths` , `suicides` , `ratio` , `skill` , `winstreak` , `losestreak` , `rounds`, `year`, `month`, `week`, `day`) SELECT `client_id` , `kills` , `deaths` , `teamkills` , `teamdeaths` , `suicides` , `ratio` , `skill` , `winstreak` , `losestreak` , `rounds`, YEAR(NOW()), MONTH(NOW()), WEEK(NOW(),3), DAY(NOW()) FROM `xlr_playerstats` """ # local variables: # tab-width: 4