Team Balancer

A place for requesting new Source.Python plugins to be made for your server.

Please request only one plugin per thread.
[+35]Jumpman
Junior Member
Posts: 14
Joined: Tue Jul 24, 2018 9:26 am

Team Balancer

Postby [+35]Jumpman » Wed May 12, 2021 6:24 pm

Hi

I use this scripts with Eventscripts for some years ago, it was the best Team Balancer i had ever use on our servers.

Maybe somebody have time to rewrite and update it so it will work again ?

ccbalance.py

Code: Select all

###################################################################################################
##
## Can's Crew Autobalancer
## - Original by
##     [cC] Sparty
##     [cC] *XYZ*SaYnt
## - 2.0 Rewrite by
##     iD|Caveman
##
###################################################################################################

import operator
import string
import sqlite3
import sys
import time
import traceback

import es
import gamethread
import playerlib
import popuplib
import psyco
import usermsg

psyco.full()

class Database(object):
    """
    Class to handle the database using sqlite3
    Does not support ':' for parameters in sql, only use '?'
    """
    def __init__(self, db_file, sql = ''):
        self.conn = sqlite3.connect(db_file)
        self.c = self.conn.cursor()
        self.c.execute(sql)
        self.conn.commit()
        self.q = []
       
    def close(self, run_queue = False):
        if run_queue:
            self.execute_queue()
        else:
            self.conn.commit()
        self.c.close()
        self.conn.close()
   
    def execute_query(self, sql, params=''):
        """
        Executes a single query without returning a value.
        """
        if string.count(sql, '?') != len(params):
            raise Exception('Incorrect number of params')
        self.c.execute(sql, params)
        self.conn.commit()

    def queue_query(self, sql, params=''):
        """
        Adds a query to the queue, to be executed later.
        """
        if string.count(sql, '?') != len(params):
            raise Exception('Incorrect number of params')
        self.q.append((sql, params))

    def execute_queue(self):
        for u in self.q:
            self.c.execute(u[0], u[1])
        self.conn.commit()
        self.q = []

    def query_row(self, sql, params=''):
        """
        Returns single row in the format ['column_name' = column_value, ].
        """
        if string.count(sql, '?') != len(params):
            raise Exception('Incorrect number of params')
        self.c.execute(sql, params)
        row = self.c.fetchone()
        if row == None:
            return None

        keys = []
        for x in self.c.description:
            keys.append(x[0])
        rtn = {}
        for k in range(len(keys)):
            rtn[keys[k]] = row[k]
        return rtn
       
    def query_value(self, sql, params=''):
        """
        Returns the first value from the first row of a query.
        """
        if string.count(sql, '?') != len(params):
            raise Exception('Incorrect number of params')
        self.c.execute(sql, params)
        r = self.c.fetchone()
        if r != None:
            return r[0]
        else:
            return None

    def query_table(self, sql, params=''):
        """
        Returns the entire results of a query in a list of tuples.
        The first tuple contains the column headings.
        """
        if string.count(sql, '?') != len(params):
            raise Exception('Incorrect number of params')
        self.c.execute(sql, params)
        r = self.c.fetchall()
        if r == None:
            return None

        keys = ()
        for x in self.c.description:
            keys = keys + (x[0], )
        rtn = []
        rtn.append(keys)
        for row in r:
            rtn.append(row)
        return rtn

class Convar(object):
    """
    Stores convar information
    """
    def __init__(self, name, default, min, max, description = ''):
        self.name = name
        self.default = default
        self.min = min
        self.max = max
        self.description = description
       
class Player(object):
    """
    Describes what we know about a player
    """
    def __init__(self, userid, steamid, team = 0):
        self.userid = userid
        self.steamid = steamid
        self.team = team

        # self.immune_for = cfg['join_immunity']
        # self.immune = True
        self.kpr = 0.5 # calculated later
        self.map_kpr = 0.5 # calculated later
        self.total_kpr = 0.5 # calculated later
       
        self.kills = 0 # set from db later
        self.rounds = 0 # set from db later
       
        self.map_rounds = 0
        self.map_kills = 0
       
        self.dead = 0
        self.changing_team = False
       
        self.last_seen = time.time()

        if db.query_value("SELECT kills FROM ccbstats WHERE steamid=? LIMIT 1", (self.steamid, )) != None:
            self.setup_from_database()

        self.set_kpr()

    def dump(self): # dump player for debugging
        self.set_kpr()
        if self.steamid in immunity_list:
            immune = True
            immune_for = immunity_list[self.steamid]
        else:
            immune = False
            immune_for = 0
        log("Player %3d: %s" % (self.userid, es.getplayername(self.userid)))
        log("          : kills=%d rounds=%d kpr=%.2f" %(self.kills, self.rounds, self.kpr))
        log("          : team=%d immune=%s immune_for=%s, dead=%s" %(self.team, immune, immune_for , self.dead))

    def show_stats(self):
        self.set_kpr()
        if self.steamid in immunity_list:
            immune = True
            immune_for = immunity_list[self.steamid]
        else:
            immune = False
            immune_for = 0
        tell(self.userid, '%s:' % es.getplayername(self.userid))
        tell(self.userid, 'Total: Kills = %d Rounds = %d KPR = %.2f' %(self.kills, self.rounds, self.total_kpr))
        tell(self.userid, '  Map: Kills = %d Rounds = %d KPR = %.2f' %(self.map_kills, self.map_rounds, self.map_kpr))
        tell(self.userid, 'Debug: team=%d immune=%s immune_for=%s, dead=%s' %(self.team, immune, immune_for, self.dead))

    def set_kpr(self):
        self.dead = int(es.getplayerprop(self.userid, 'CCSPlayer.baseclass.pl.deadflag'))
        if self.rounds == 0: # completely new player
            self.kpr = 0.5 # guess
        elif self.map_rounds == 0: # new on map
            self.kpr = float(self.kills)/float(self.rounds) # use only saved info
        else:
            self.total_kpr = float(self.kills)/float(self.rounds)
            self.map_kpr = float(self.map_kills)/float(self.map_rounds)
            if cfg["use_average_performance"]:
                self.kpr = (self.total_kpr + self.map_kpr) / 2
            else:
                self.kpr = self.total_kpr

    def add_kill(self):
        if es.exists("variable", "ccw_warmup") and cfg["ignore_warmup"] == 1: # running warmup plugin
            if es.getInt("ccw_warmup") == 1:
                return # do not do anything.
        self.map_kills +=1
        self.kills += 1
   
    def del_round(self):
        self.map_rounds -= 1
        self.rounds -= 1
   
    def spawn(self):
        if es.exists("variable", "ccw_warmup") and cfg["ignore_warmup"] == 1: # running warmup plugin
            if es.getInt("ccw_warmup") == 1:
                return # do not do anything.
        self.map_rounds += 1
        self.rounds += 1
        if self.changing_team:
            self.changing_team = False
            if cfg['notify_team_change']:
                self.notify_team_change()

    def notify_team_change(self):
        self.changing_team = False
        if self.team == 2:
            usermsg.fade(self.userid,1,1000,100,255,0,0,60) # flash screen
            if cfg['notify_team_change'] > 1:
                es.playsound(self.userid,"common/foghorn.wav",1) # play sound
            if cfg['notify_team_change'] > 2:
                es.centertell(self.userid,"You have been switched to the T side.") # show a message
        if self.team == 3:
            usermsg.fade(self.userid,1,1000,100,0,0,255,60)
            if cfg['notify_team_change'] > 1:
                es.playsound(self.userid,"common/foghorn.wav",1)
            if cfg['notify_team_change'] > 2:
                es.centertell(self.userid,"You have been switched to the CT side.")

    def make_invulnerable(self):
        # es.setplayerprop(str(self.userid),"CBasePlayer.m_iHealth","1000") # gives 1000 hp. Caused too many comments on my server, so disabled.
        es.setplayerprop(str(self.userid),"CBaseAnimating.m_nHitboxSet",2)

    def make_vulnerable(self):
        es.setplayerprop(str(self.userid),"CBaseAnimating.m_nHitboxSet",0)

    def swap(self):
        if es.exists("command", "ma_swapteam"): # server is running mani
            es.server.queuecmd("ma_swapteam %s" % str(self.userid)) # use the mani command
        elif es.exists("command", "teamswitch"): # sourcemod TeamSwitch plugin https://forums.alliedmods.net/showthread.php?t=67292
            es.server.queuecmd("teamswitch %s" % es.getplayername(self.userid))
        else:       
            if self.team == 2:
                new_team = 3
            elif self.team == 3:
                new_team = 2
            es.server.queuecmd('es_xchangeteam %d %d' % (self.userid, new_team))
            # es.changeteam(str(self.userid), str(new_team)) # use the eventscripts command

        self.changing_team = True
        # self.immune = True
        # self.immune_for = cfg['swap_immunity']
        immunity_list[self.steamid] = cfg['swap_immunity']
       
    def setup_from_database(self):
        """
        Reads any stored data about a player from the database.
        """
        info = db.query_row('SELECT total(kills), total(rounds) FROM ccbstats WHERE steamid = ?', (self.steamid, ))
        self.kills = int(info['total(kills)'])
        self.rounds = int(info['total(rounds)'])
   
    def write_to_database(self):
        """
        Write player information into the database for cold storage.
        """
        if (self.map_kills > 0 or self.map_rounds > 0) and self.steamid != 'BOT':
            db.queue_query("INSERT INTO ccbstats (steamid, kills, rounds, timestamp) VALUES (?, ?, ?, ?)", (self.steamid, self.map_kills, self.map_rounds, self.last_seen))

class Team(object):
    """
    Represents the information about a team
    """
    def __init__(self, team):
        self.team = team
        self.size = es.getplayercount(str(team))
        self.strength = 0.0
        self.weighting = 0.0
        self.set_strength()

    def set_strength(self):
        for player in connected_players.itervalues():
            if player.team == self.team:
                self.strength += player.kpr
       
class Teams(object):
    """
    Represents all the information about teams for balancing calculations so it only needs to be calculated once.
    """
    def __init__(self):
        self.team = {}
        self.team[2] = Team(2) # Ts
        self.team[3] = Team(3) # CTs
        self.larger = 0
        self.stronger = 0
        self.strength_dif = self.team[2].strength - self.team[3].strength
        self.strengths_balance = 0.0
        self.numbers_balanced = True
       
        self.set_larger()
        self.set_stronger()
        self.set_numbers_balanced()
        self.set_strengths_balance()
        self.team[2].strength = self.team[2].strength * map_bias
       
    def set_numbers_balanced(self):
        if abs(self.team[2].size - self.team[3].size) > cfg['max_number_imbalance']:
            self.numbers_balanced = False
        else:
            self.numbers_balanced = True

    def set_strengths_balance(self):
        if self.team[2].strength != 0 and self.team[3].strength != 0:
            self.strengths_balance = min(self.team[2].strength, self.team[3].strength) / max(self.team[2].strength, self.team[3].strength) * 100.0
           
    def set_larger(self):
        if self.team[2].size > self.team[3].size:
            self.larger = 2
        elif self.team[2].size < self.team[3].size:
            self.larger = 3

    def set_stronger(self):
        if self.team[2].strength > self.team[3].strength:
            self.stronger = 2
            self.team[2].weighting = cfg['better_factor']
            self.team[3].weighting = cfg['worse_factor']
        elif self.team[2].strength < self.team[3].strength:
            self.stronger = 3
            self.team[2].weighting = cfg['worse_factor']
            self.team[3].weighting = cfg['better_factor']
       
class Merit(object):
    """
    Holds information on the merit of switching a particular t and ct
    """
    def __init__(self, new_align, immune_penalty, p1, p2 = None):
        self.p1 = p1 # userid of the first player to be swapped
        self.p2 = p2 # userid of the second to be swapped, if there is one
        self.align = new_align # difference between teams after making swap (new_t_team_strength - new_ct_team_strength)
        self.score = abs(self.align) # align as positive number for comparing
        if cfg['immunity_type'] == 1: # if immunity_type is set to weight against swapping players
            self.score = self.score + float(immune_penalty) * cfg['immunity_weight']

########################################### Global Vars ###########################################

info = es.AddonInfo()
info['name'] = 'cC Team Balancer'
info['version'] = '2.4.0'
info['author'] = '*XYZ*SaYnt & iD|Caveman'
info['url'] = 'http://addons.eventscripts.com/addons/view/ccbalance'
info['basename'] = 'ccbalance'
info['msg_prefix'] = '[cCB]'
info['cvar_prefix'] = 'ccb_'
info['description'] = 'Team balancing solution for CS:S'

convars = {}
cfg = {}
maps = {}
immunity_list = {}
map_bias = 1.0
map_rounds = 0
balance_in = 0
connected_players = {}
disconnected_players = {}
db = Database('%s/%s' %(es.getAddonPath(info['basename']), 'ccbalance.sqldb'), "CREATE TABLE IF NOT EXISTS ccbstats (steamid TEXT NOT NULL, kills INTEGER NOT NULL DEFAULT 0, rounds INTEGER NOT NULL DEFAULT 0, timestamp REAL DEFAULT ((julianday('now') - 2440587.5)*86400.0))")

########################################### Load/Unload ###########################################

def load():
    """
    EVENT: executes whenever the script is loaded via es_load to perform initializations.
    """

    setup_cvars() # create all the cvars and set them to default values
    es.server.queuecmd('exec ccbalance.cfg')

    if not es.exists('command', '%sdumpstats' % info['cvar_prefix']):
        es.regcmd('%sdumpstats' % info['cvar_prefix'], '%s/dump_stats' % info['basename'], 'Shows all players stats for debugging.')
   
    if not es.exists('command', '%sprunedb' % info['cvar_prefix']):
        es.regcmd('%sprunedb' % info['cvar_prefix'], '%s/prune_db' % info['basename'], 'Manually delete all records older than the specified number of days.')

    if not es.exists('command', '%sshowplayer' % info['cvar_prefix']):
        es.regcmd('%sshowplayer' % info['cvar_prefix'], '%s/show_player' % info['basename'], 'Show the stats of a single player given the steamid.')

    if not es.exists('command', '%ssetmapbias' % info['cvar_prefix']):
        es.regcmd('%ssetmapbias' % info['cvar_prefix'], '%s/set_map_bias' % info['basename'], 'Set the bias for a particular map.')

    if not es.exists('clientcommand', '%sswapmenu' % info['cvar_prefix']):
        es.regclientcmd('%sswapmenu' % info['cvar_prefix'], '%s/swapmenu' % info['basename'], 'Show a list of players to swap.')

    es.regsaycmd("ccbstats", "%s/show_stats" % info['basename'], "Shows a player their own team balance information") # create say command to show a player their stats
   
    es.addons.registerClientCommandFilter(ClientCommandFilter)
   
    # perform any init that needs to be done if we are loaded when a game is already in progress.
    mid_game_load()
   
    logmsg("cC Team Balancer %s loaded." % info['version'], 0)
    es.set("%sversion" % info['cvar_prefix'], info['version'], 'The version of %s being run.' % info['name'])
    es.makepublic("%sversion" % info['cvar_prefix'])

def unload():
    """
    EVENT: executes whenever the script is unloaded via es_unload
    Perform any necessary cleanup.
    """
    database_dump_timer() # Write everyone in memory to the database
   
    db.close(True) # close the database and run the queue if there is one

    es.unregsaycmd('ccbstats') #clean up the say command.
   
    es.addons.unregisterClientCommandFilter(ClientCommandFilter)
   
    logmsg("cC Team Balancer %s unloaded." % info['version'], 0)

############################################# Events ##############################################

def server_cvar(ev):
    """
    If the cvar changed is one for the balancer, update internal config.
    """
    if ev['cvarname'][:len(info['cvar_prefix'])] == info['cvar_prefix']: # if it is a cCBalance var
        key = ev['cvarname'][len(info['cvar_prefix']):] # cvarname minus the first 4 characters = key in cfg dict
        if key == 'version':
            return
        var = convars[key]
        if key == 'global_immunity' or key == 'admins': # global immunity and admins need string handling
            cfg[key] = ev['cvarvalue'].replace(' ', '').split(',')
            log('%s = "%s"' %(key, ev['cvarvalue']))
            return
        elif var.name in ['better_factor', 'worse_factor', 'immunity_weight', 'acceptable_strength_imbalance', 'unacceptable_strength_imbalance']:
            val = float(ev['cvarvalue'])
        else:
            val = int(ev['cvarvalue'])
        if (val <= var.max or var.max == None) and val >= var.min: # check for acceptable value
            cfg[key] = val
            if key == 'swap_immunity': # if it is the length of time a player is immune for after a swap
                for id in immunity_list:
                    if immunity_list[id] > val: # and the player is immune for longer than the current max
                        immunity_list[id] = val # set the players immunity to the current max
        else:
            if var.max == None:
                log('"%s" must be larger than %d' %(key, var.min))
            else:
                log('"%s" must be larger than %d and smaller than %d' %(key, var.min, var.max))

def es_map_start(ev):
    """
    EVENT: executes whenever the map starts.
    Reset all of the counters and flags that we need to.
    """
    global map_bias
    global map_rounds
    global balance_in

    if ev['mapname'] in maps:
        map_bias = maps[ev['mapname']]
    else:
        map_bias = 1.0

    logmsg('map_bias set to %s' %map_bias)
       
    map_rounds = 0
    balance_in = cfg['run_frequency']
    database_dump_timer() # dump all player info to the database and start again
    delete_older(cfg['stats_days'])

def player_team(ev):
    """
    EVENT: executed whenever a player joins a team
    Update our player data structures accordingly.
    """
    if ev['team'] != '0': # if not joining unassigned
        userid = int(ev['userid'])
        team = int(ev['team'])
        if userid in connected_players:
            connected_players[userid].team = team # give them their new team
        else:
            add_player(userid, ev['es_steamid'], team)

def player_disconnect(ev):
    """
    EVENT: Executes whenever a player leaves
    Update our player data structures accordingly.
    """
    userid = int(ev['userid'])
    if userid in connected_players: # just occasionally a player leaves before joining a team and is therefore not in our data structures.
        connected_players[userid].last_seen = time.time() # remember the time they left
        disconnected_players[connected_players[userid].steamid] = connected_players[userid] # store them
        del connected_players[userid] # delete them from connected players

###################################################################################################

def round_start(ev):
    """
    EVENT: executes whenever a round starts
    Increment the number of rounds that have been played for this map.
    """
    global map_rounds
    global balance_in

    map_rounds += 1
    balance_in -= 1
    if balance_in < 0:
        balance_in = 0
    for player in connected_players.itervalues(): # from everyone,
        player.make_vulnerable() # remove invulnerability

def player_spawn(ev):
    """
    EVENT: executed whenever a player spawns to record the fact that the player played this round.
    """
    userid = int(ev['userid'])
    if userid in connected_players:
        connected_players[userid].spawn()
    else:
        add_player(userid, ev['es_steamid'], int(ev['es_userteam']))

def player_death(ev):
    """
    EVENT: executes every time a player dies to record a kill.
    """
    attacker = int(ev['attacker'])
    victim = int(ev['userid'])
    if victim == attacker: # if it is a suicide
        return
    if attacker: # if there was an attacker
        if connected_players[victim].team != connected_players[attacker].team: # it wasn't a TK
            connected_players[attacker].add_kill() # give them a kill

def round_end(ev):
    """
    Round end processing, then triggers delayed calculations.
    """
    global immunity_list
    x = []
    for id in immunity_list:
        immunity_list[id] -= 1
        if immunity_list[id] <= 0:
            x.append(id)
    for id in x:
        del immunity_list[id]
   
    global map_rounds
    reason = int(ev['reason'])
    if reason == 9 or reason == 15: # if draw or game commence
        if map_rounds > 0:
            map_rounds -= 1 # remove map round
        for player in connected_players.itervalues():
            player.del_round() # remove round from players
        return

    if not cfg["stats_only"]:
        gamethread.delayed(1.0, round_end_timer, reason) # Wait 1 second then start balancing process
    else:
        logmsg('Balancing is disabled. %s is just gathering stats.' % info['name'], 2)

############################################# Messages ############################################

def logmsg(s, verb = 3):
    """
    Record a message to the server log, and maybe show it to all connected_players.
    """
    if cfg['verbose'] >= verb:
        msg(s)
    else:
        log(s)

def msg(msg):
    """
    Show a message to all connected_players.
    """
    es.msg('#multi', '#green%s #default%s' %(info['msg_prefix'], msg))

def log(msg):
    """
    Record a message to the server log.
    """
    # this does not work on my local dedi
    es.log('%s %s' %(info['msg_prefix'], msg))
   
    # workaround
    es.server.queuecmd('echo "%s %s"' %(info['msg_prefix'], msg))

def tell(userid, msg):
    """
    Tell a single player something.
    """
    es.tell(userid, '#multi', '#green%s #default%s' %(info['msg_prefix'], msg))

############################################ Balancing ############################################

def round_end_timer(reason):
    start = time.time()
    delayed_round_end(reason)
    stop = time.time()
    log("End of round processing took %f seconds" % (stop-start))

def delayed_round_end(reason):
    """
    Keep track of the number of rounds each player has played, and compute their
    kill rates.
    If it is time, spring into action and do some team balancing.
    """
    global map_rounds
    global balance_in

    for player in connected_players.itervalues():
        player.set_kpr() # set kpr for every player

    teams = Teams() # only calculate all the team info once in the balancing process rather than for every swap.

    logmsg("Round %d: Team Strengths: T = %.2f | CT = %.2f."  % (map_rounds, teams.team[2].strength, teams.team[3].strength), 3)

    if teams.team[2].size + teams.team[3].size < cfg['min_player_count']: # check enough players to balance
        logmsg("Fewer than %d players. Not balancing." % cfg['min_player_count'], 1)
        return
   
    if es.exists("variable", "ccw_warmup") and cfg["ignore_warmup"] == 1: # running warmup plugin
        if es.getInt("ccw_warmup") == 1:
            logmsg("Currently in warmup. Not Balancing.")
            return # do not do anything.

    report = "Balancing in %s rounds." % balance_in # default message
    balance_this_round = False
    if balance_in <= 0: # if a round to balance on
        balance_this_round = True
        if teams.strengths_balance > cfg['acceptable_strength_imbalance']: # if team strengths are close enough
            report = "No balancing needed; the teams are balanced to %.1f percent." %( 100 - teams.strengths_balance)
            balance_this_round = False

    if not teams.numbers_balanced: # if team numbers are severely imbalanced
        report = "Imbalance in team counts. Balancing this round."
        balance_this_round = True
    elif teams.strengths_balance < cfg['unacceptable_strength_imbalance']: # if team strengths are severely imbalanced
        report = "Severe imbalance in team strengths. Balancing this round."
        balance_this_round = True

    logmsg(report, 1)
    if balance_this_round:
        do_balance(teams)
        balance_in = cfg['run_frequency']

def do_balance(teams):
    """
    compute merit scores of trading players and do the best trade
    """
    merit = []
    if map_rounds == 1:
        obey_immunity = 0
    else:
        obey_immunity = cfg['immunity_type']

    # compute all possible player moves.  We need to only allow moves that will
    # satisfy team-number-balance constraints.
    for player in connected_players.itervalues():
        player.dead = int(es.getplayerprop(player.userid, 'CCSPlayer.baseclass.pl.deadflag'))
        if player.team == teams.larger: #if the team the player is on is the larger
            compute_merit_move(merit, teams, player, obey_immunity) # test moving him

    # compute all possible swaps.  We only allow swaps if the current team counts
    # are within our allowed limits.  Otherwise, we work only with player moves
    # as computed above.
    if teams.numbers_balanced:
        swaps_tested = 0
        try:
            for p_t in connected_players.itervalues():
                if p_t.team == 2: # iterate through terrorists
                    for p_ct in connected_players.itervalues():
                        if p_ct.team == 3: # one is on T and the other on CT
                            swaps_tested += compute_merit_swap(merit, teams, p_t, p_ct, obey_immunity) # so test swapping them
                            if swaps_tested >= cfg['max_swaps_considered']: # if tested enough
                                raise UserWarning # quit loop
        except UserWarning:
            pass

    if merit: # if we have any merits
        best = 0
        topscore = 10000.0
        for i, v in enumerate(merit): # in one pass, figure out who has the best merit score.
            if v.score < topscore:
                topscore = v.score
                best = i

        m = merit[best]
        logmsg("%d possibilities tested." % len(merit), 3)
        logmsg("Best align found = %.1f, strengthdiff=%.1f" % (m.align, teams.strength_dif), 2)

        if abs(m.align) < 0.7 * abs(teams.strength_dif) or not teams.numbers_balanced:
        # only do the swapping if the new projected numbers are significantly better than the
        # current strength difference, or if the team numbers are imbalanced.
            p1 = m.p1 # p1 is always present
            if m.p2 != None: # if there is a second player to move
                p2 = m.p2
                logmsg("Swapping %s for %s." % (es.getplayername(p1), es.getplayername(p2)), 2) # it is a swap
                connected_players[p2].swap() # move the second player
            else:
                logmsg("Moving %s." % es.getplayername(p1), 2) # otherwise it's just a move
            connected_players[p1].swap() # move the one that's always present

            for player in connected_players.itervalues():
                if not player.dead:
                    player.make_invulnerable()
        else:
            logmsg("Team swap cancelled: moving players would not significantly help imbalance.", 1)
    else:
        logmsg('Unable to list any valid moves or swaps.', 1)

def compute_merit_move(merit, teams, p, obey_immunity):
    """
    compute the merit of moving a single player
    """
    if cfg['wait_until_dead']: # if this player is alive, dont allow him to be swapped
        if not p.dead:
            return
    if p.steamid in cfg['global_immunity'] and obey_immunity: # if the player is on the immunity list, don't swap
        return
    # if p.immune and obey_immunity == 2: # if this player is temporarily immune, don't swap them
    if p.steamid in immunity_list and obey_immunity == 2: # if this player is temporarily immune, don't swap them
        return

    if p.team == 2: # if player is currently a T
        projected_gain = p.kpr * teams.team[3].weighting # players killrate on new team
        new_align = (teams.team[2].strength - p.kpr) - (teams.team[3].strength + projected_gain) # calculate (new T team killrate) - (new CT team killrate)
    elif p.team == 3: # if player is currently a CT
        projected_gain = p.kpr * teams.team[2].weighting # players killrate on new team
        new_align = (teams.team[2].strength + projected_gain) - (teams.team[3].strength - p.kpr)# calculate (new T team killrate) - (new CT team killrate)
    if p.steamid in immunity_list:
        immunity = immunity_list[p.steamid]
    else:
        immunity = 0
    merit.append(Merit(new_align, immunity, p.userid)) # put into merit object

def compute_merit_swap(merit, teams, p_t, p_ct, obey_immunity):
    """
    compute the merit of swapping two players
    """
    if cfg['wait_until_dead']:
        if not p_t.dead or not p_ct.dead: # if either player is alive, dont bother testing swap
            return 0
    if (p_t.steamid in cfg['global_immunity']) or (p_ct.steamid in cfg['global_immunity']) and obey_immunity: # if either player is on the immunity list, don't both testing the swap
        return 0
    # if (p_t.immune or p_ct.immune) and obey_immunity == 2: # if either player is temporarily immune, don't bother swapping them
    if (p_t.steamid in immunity_list) or (p_ct.steamid in immunity_list) and obey_immunity == 2: # if either player is on the immunity list, don't both testing the swap
        return 0

    t_team_projected_gain = p_ct.kpr * teams.team[2].weighting # players killrate on new team
    ct_team_projected_gain = p_t.kpr * teams.team[3].weighting # players killrate on new team
    new_align = ((teams.team[2].strength - p_t.kpr) + t_team_projected_gain) - ((teams.team[3].strength - p_ct.kpr) + ct_team_projected_gain) # calculate (new T team killrate) - (new CT team killrate)
    if p_t.steamid in immunity_list:
        immunity = immunity_list[p_t.steamid]
    else:
        immunity = 0
    if p_ct.steamid in immunity_list:
        immunity += immunity_list[p_ct.steamid]
    merit.append(Merit(new_align, immunity, p_t.userid, p_ct.userid)) # create merit object
    return 1

def handle_join(userid):
    """
    Handle the autoassign and sort the player into reasonable teams if it will not create a numerical imbalance.
    """
    if playerlib.getPlayer(userid).teamid in (2, 3):
        tell(userid, 'You are already on a team, you cannot autoassign')
        return

    # Get current balance information.
    teams = Teams()
   
    if teams.larger == 0: # there is not a larger team
        # Calculate team strengths if the player was put on either
        new_t_str = teams.team[2].strength + (connected_players[userid].kpr * map_bias) # have to allow for map bias only for new player as the team strength has it already applied
        new_ct_str = teams.team[3].strength + connected_players[userid].kpr
       
        # Calculate strength differences
        strdif_if_ct = teams.team[2].strength - new_ct_str
        strdif_if_t = new_t_str - teams.team[3].strength
       
        # put the player on the team that is the least imbalanced.
        if abs(strdif_if_ct) < abs(strdif_if_t):
            es.server.queuecmd('es_xchangeteam %d %d' % (userid, 3))
        else:
            es.server.queuecmd('es_xchangeteam %d %d' % (userid, 2))
    elif teams.larger == 2:
        es.server.queuecmd('es_xchangeteam %d %d' % (userid, 3))
    elif teams.larger == 3:
        es.server.queuecmd('es_xchangeteam %d %d' % (userid, 2))

############################################ Admin Menu ###########################################

def menuhandler(userid, choice, popupname):
    """
    handle the menu input and swap the chosen player.
    """
    if choice in connected_players:
        connected_players[choice].swap()
        # Will not kill the player if mani is running, so it should give them the coloured overlay here as well as on spawn.
    else:
        es.tell(userid, 'Your selection could not be found.')
       
    try:
        popuplib.delete(popupname)
    except ValueError:
        logmsg(0, 'Popup did not exist...')
 
def swapmenu():
    """
    Give an admin a menu they can use to swap a player to the opposite team.
    """
    if cfg["enable_swap_menu"] == 0:
        return
    userid = es.getcmduserid()
    if not playerlib.getPlayer(userid).steamid in cfg['admins']:
        tell(userid, 'You do not have permission to run this command.')
        return
   
    players = {}
    pl_list = playerlib.getPlayerList('#t') # get a list of all Ts
    for pl in pl_list: # work through all currently connected players
        players[pl.userid] = pl.name
    pl_list = playerlib.getPlayerList('#ct') # get a list of all CTs
    for pl in pl_list: # work through all currently connected players
        players[pl.userid] = pl.name

    sorted_players = sorted(players.items(), key = operator.itemgetter(1)) # sort the list by name

    swapmenu = popuplib.easymenu('ccb_swapmenu_%d' % userid, None, menuhandler)
    swapmenu.settitle('Select a player to swap:')
    # Create the popup
    for pl in sorted_players:
        swapmenu.addoption(pl[0], pl[1])
    # send it to the admin
    swapmenu.send(userid)

########################################### Other Funcs ###########################################

def ClientCommandFilter(userid, arguments):
    """
    Filter client jointeam commands and force autoassign
    """
    r = True
    if arguments[0].lower() == "jointeam":
        if cfg['force_autoassign'] and not connected_players[userid].steamid in cfg['admins']:
            if len(arguments) > 1 and arguments[1].isdigit() and int(arguments[1]) in (2, 3): # T, CT
                r = False
                es.centertell(userid, "[cCB] Please use Autoassign")
                tell(userid, "Please use Autoassign")
            elif len(arguments) > 1 and arguments[1].isdigit() and int(arguments[1]) == 0: # Auto
                r = False
                handle_join(userid)
    return r

def set_map_bias():
    """
    set a bias for a specific map.
    """
    if es.getargc() != 3:
        log('Error: ccb_setmapbias takes precisely 2 arguments.')
        return
   
    maps[es.getargv(1)] = float(es.getargv(2))
    log('Bias for %s set to %s' %(es.getargv(1), es.getargv(2)))

def prune_db():
    """
    remove all records older than out cutoff from the databse to stop it from getting too bloated
    """
    if es.getargc() == 1:
        delete_older(cfg['stats_days'])
    elif es.getargc() == 2:
        delete_older(es.getargv(1))
    else:
        log('Error! ccb_prune_db requires 0 or 1 arguments. Correct: ccb_prune_db <days to keep>')

def delete_older(days = 0):
    """
    remove all records older than out cutoff from the databse to stop it from getting too bloated
    """
    if days != 0:
        db.execute_query('DELETE FROM ccbstats WHERE timestamp < ?', (time.time() - (86400 * days), ))

def show_player():
    """
    Show a players stats from the db
    """
    if es.getargc() != 2:
        log('Error. ccb_show_player requires a steamid. If there is one, enclose it in double quotes. "STEAM_X" is valid where STEAM_X is not.')
        log(str(es.getargc()))
        return

    steamid = es.getargv(1)
    info = db.query_row('SELECT total(kills), total(rounds) FROM ccbstats WHERE steamid = ?', (steamid, ))
    log('%s - Player has %s kills over %s rounds.' %(steamid, int(info['total(kills)']), int(info['total(rounds)'])))

def add_player(userid, steamid, team):
    """
    Sets up a new player when they are first captured by an event.
    Reuses old Player() if it is a player rejoining in the same map.
    """
    if not userid in connected_players: # if it's a new player
        if not steamid in disconnected_players: # if it's not an old player rejoining
            connected_players[userid] = Player(userid, steamid, team) # create a new entry for them
        else: # otherwise, it's an old one coming back
            connected_players[userid] = disconnected_players[steamid] # so move them back into current use
            connected_players[userid].userid = userid
            del disconnected_players[steamid] # and delete them from storage

def dump_stats():
    """
    print a list of all connected players and their info into the server console for debugging.
    """
    for player in connected_players.itervalues():
        player.dump()
           
def show_stats():
    """
    show a player the current info about their performance
    """
    if cfg['enable_say_command']:
        connected_players[es.getcmduserid()].show_stats()

def setup_cvars():
    """
    Create and set all cvars to their default values.
    """
    convars["acceptable_strength_imbalance"] = Convar("acceptable_strength_imbalance", 75.0, 0.0, 100.0, 'If the weaker team is more than this % as strong as the stronger team, scheduled balancing will be skipped.')
    convars["admins"] = Convar("admins", 'STEAM_0:0:631910, STEAM_0:0:15764696', None, None, 'List of steamids who are immune to forced autoassign and can use the ccb_swapteam console command.')
    convars["better_factor"] = Convar("better_factor", 1.1, 1.0, 2.0, 'How much a player improves when put onto the winning team. Recommended value 1.1.')
    convars["enable_say_command"] = Convar("enable_say_command", 1, 0, 1, 'Enable or disable the ccbstats chat command.')
    convars["enable_swap_menu"] = Convar("enable_swap_menu", 0, 0, 1, 'Enable or disable the ccb_swapmenu command for admins.')
    convars["force_autoassign"] = Convar("force_autoassign", 1, 0, 1, 'Force people to select autoassign to join a team. cCB will then attempt to sort people into roughly even teams.')
    convars["global_immunity"] = Convar("global_immunity", 'STEAM_0:0:1, STEAM_0:1:2', None, None, 'List of steamids not considered for swaps. Seperate multiple IDs with commas.')
    convars["ignore_warmup"] = Convar("ignore_warmup", 1, 0, 1, 'Ignore the warmup (only works with CCWarmup)')
    convars["immunity_type"] = Convar("immunity_type", 0, 0, 2, 'How to handle immunity.\n\t0: Ignore all immunity\n\t1: Temporary (join and swap) immunity only makes it less likely a player will be swapped. Being on the global immunity list still means full immunity\n\t2: Immune players are never swapped.')
    convars["immunity_weight"] = Convar("immunity_weight", 0.15, 0.0, 1.0, 'If ccb_immunity_type = 1 then this is the penalty applied to a potential swap for every round of immunity left. Recommended 0.1 -> 0.3')
    convars["join_immunity"] = Convar("join_immunity", 1, 1, None, 'A player is immune to being moved for this many rounds after joining.')
    convars["max_number_imbalance"] = Convar("max_number_imbalance", 1, 1, None, 'Maximum numerical difference between teams that the balancer will tolerate.')
    convars["max_swaps_considered"] = Convar("max_swaps_considered", 500, 1, None, 'Maximum swaps considered each round. If your server lags at round end, reduce this number.') # done 256 merits (31 players) in < 0.01 seconds on my home pc
    convars["min_player_count"] = Convar("min_player_count", 3, 2, None, 'Minimum number of players before balancing will occur.')
    convars["notify_team_change"] = Convar("notify_team_change", 3, 0, 3, 'How to inform a player that he/she has been swapped.\n\t0: No notification.\n\t1: Notification with a coloured overlay at first spawn\n\t2: Notification with a coloured overlay and a brief sound.\n\t3: Notification with a coloured overlay, sound and a centered message.')
    convars["run_frequency"] = Convar("run_frequency", 2, 1, None, 'How frequently balancing will occur, in rounds.')
    convars["stats_only"] = Convar("stats_only", 0, 0, 1, 'If 1, the script will not attempt to balance teams, only collect stats.')
    convars["stats_days"] = Convar("stats_days", 0, 0, None, 'How many days to store stats. Set to 0 to store stats permanently.')
    convars["swap_immunity"] = Convar("swap_immunity", 2, 1, None, 'A player is immune to being moved for this many rounds after a swap.')
    convars["use_average_performance"] = Convar("use_average_performance", 1, 0, 1, 'If 1, a players kpr will be an average of their overall and map kprs, otherwise it is their overall kpr. When 1, it makes the balancer more aggressive.')
    convars["verbose"] = Convar("verbose", 3, 0, 3, 'Governs how verbose the balancer is. Reduce this number if you are seeing too much output.')
    convars["wait_until_dead"] = Convar("wait_until_dead", 2, 0, 1, 'Only swap dead players. Swapping live players will kill them unless you have a seperate plugin.')
    convars["worse_factor"] = Convar("worse_factor", 0.9, 0.1, 1.0, 'How much a player worsens when put onto the losing team. Recommended value 0.9.')
    convars["unacceptable_strength_imbalance"] = Convar("unacceptable_strength_imbalance", 60, 0.0, 100.0, 'If the weaker team is less than this % as strong as the stronger team, an emergency balance will occur.')
    for var in convars.itervalues(): # create server variables for all the items in the convars dict
        es.set('%s%s' % (info['cvar_prefix'], var.name), var.default, var.description)
        es.flags('add', 'notify', '%s%s' % (info['cvar_prefix'], var.name)) # notify changes to the cvar, required to trigger server_cvar
        if not var.name in ('admins', 'global_immunity'):
            cfg[var.name] = var.default
        else:
            cfg[var.name] = var.default.replace(' ', '').split(',')

def mid_game_load():
    """
    Setup the script when loaded mid game
    """
    pl_list = playerlib.getPlayerList('#all') # get a list of all connected players
    for pl in pl_list: # work through all currently connected players
        userid = int(pl.userid)
        connected_players[userid] = Player(userid, pl.attributes['steamid'], pl.attributes['teamid']) # put them in our data structure

def database_dump_timer():
    start = time.time()
    write_players()
    stop = time.time()
    log("Databse Dump took %f seconds" % (stop-start))

def write_players():
    for player in connected_players.itervalues(): # for every connected player
        player.write_to_database() # queue writing them to the db
    connected_players.clear() # empty the list
    for player in disconnected_players.itervalues(): # same for disconnected
        player.write_to_database()
    disconnected_players.clear()
    db.execute_queue() # run the queue


ccbalance.cfg

Code: Select all

///////////////////////////////////////////////////////////////////////////////////////////////////
// Can's Crew Autobalancer
// - Original by
//     [cC] Sparty
//     [cC] *XYZ*SaYnt
// - 2.0 rewrite by
//     iD|Caveman
// Edited by Jumpman 1 February 2015
///////////////////////////////////////////////////////////////////////////////////////////////////

/////////////////////////////////////////// Balancing /////////////////////////////////////////////

// If 1, the script will not attempt to balance teams, only collect stats. (Default 0)
ccb_stats_only 0

// How frequently balancing will occur, in rounds. (Default 4)
ccb_run_frequency 2

// Minimum number of players before balancing will occur. (Default 6)
ccb_min_player_count 3

// Only swap dead players. Swapping live players will kill them unless you have a seperate plugin. (Default 0)
ccb_wait_until_dead 1

// If 1, a players kpr will be an average of their overall and map kprs, otherwise it is their overall kpr. When 1, it makes the balancer more aggressive. (Default 1)
ccb_use_average_performance 1

// If the weaker team is more than this 75% as strong as the stronger team, scheduled balancing will be skipped. (Default 90)
ccb_acceptable_strength_imbalance 75

// If the weaker team is less than this 60% as strong as the stronger team, an emergency balance will occur. (Default 60)
ccb_unacceptable_strength_imbalance 60

// Maximum numerical difference between teams that the balancer will tolerate. (Default 1)
ccb_max_number_imbalance 1

// Maximum swaps considered each round. If your server lags at round end, reduce this number. (Default 500)
ccb_max_swaps_considered 500

// How much a player improves when put onto the winning team. Recommended value 1.1. (Default 1.1)
ccb_better_factor 1.1

// How much a player worsens when put onto the losing team. Recommended value 0.9 (Default 0.9)
ccb_worse_factor 0.9

//////////////////////////////////////////// Immunity /////////////////////////////////////////////

// List of steamids not considered for swaps. Seperate multiple IDs with commas. (Default 1)
// e.g. ccb_global_immunity "STEAM_0:0:1, STEAM_0:1:2"
ccb_global_immunity ""

// A player is immune to being moved for this many rounds after joining (Default 3)
ccb_join_immunity 1

// A player is immune to being moved for this many rounds after a swap. (Default 20)
ccb_swap_immunity 2

// How to handle immunity.
// 0: Ignore all immunity
// 1: Temporary (join and swap) immunity only makes it less likely a player will be swapped. Being on the global immunity list still means full immunity
// 2: Immune players are never swapped.
ccb_immunity_type 0

// If ccb_immunity_type = 1 then this is the penalty applied to a potential swap for every round of immunity left.
ccb_immunity_weight 0.15

///////////////////////////////////////// Notifications ///////////////////////////////////////////

// Governs how verbose the balancer is. Reduce this number if you are seeing too much output ingame. (Default 3)
ccb_verbose 3

// Enable or disable the "ccbstats" chat command.
ccb_enable_say_command 1

// How to inform a player that he/she has been swapped. (Default 1)
// 0: No notification.
// 1: Notification with a coloured overlay at first spawn
// 2: Notification with a coloured overlay and a brief sound.
// 3: Notification with a coloured overlay, sound and a centered message.
ccb_notify_team_change 3

// Ignore the warmup only works with CCWarmup (Added by Jumpman)
ccb_ignore_warmup 1

// Force people to select autoassign to join a team. cCB will then attempt to sort people into roughly even teams (Added by Jumpman)
ccb_force_autoassign 1

// How many days to store stats. Set to 0 to store stats permanently. (Default 28)
ccb_stats_days 0

// Enable or disable the ccb_swapmenu command for admins (Added by Jumpman)
ccb_enable_swap_menu 0

// List of steamids who are immune to forced autoassign and can use the ccb_swapteam console command
// ccb_admins "STEAM_0:1:8690814, STEAM_0:1:11655561, STEAM_0:0:6850792"
ccb_admins ""

///////////////////////////////////////// Map Biases ///////////////////////////////////////////
// Set map biases here. Defaults to 1.0 (even) for maps not listed.
// Use a value of less than 1.0 to give a T advantage, and greater than 1.0 to give a CT advantage.
// Use values further away from 1.0 to compensate for a greater imbalance. These are the values I use on my 32man server, smaller or larger servers will need different values for the same maps.
ccb_setmapbias de_aztec 0.9
ccb_setmapbias de_dust 0.9
ccb_setmapbias de_tides 1.15
ccb_setmapbias de_prodigy 0.9
ccb_setmapbias de_cbble 0.95
ccb_setmapbias cs_assault 1.15
ccb_setmapbias cs_compound 1.15
ccb_setmapbias de_autumn 1.05
ccb_setmapbias de_chateau 0.95
ccb_setmapbias cs_italy 1.10

Return to “Plugin Requests”

Who is online

Users browsing this forum: No registered users and 17 guests