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