/*
	// misc
	admin_teambalance - show PTB statistics (ACCESS_ALL)
	admin_teambalance status - show all current settings (ACCESS_ALL)
	admin_teambalance list - list of ptb commands (ACCESS_ALL)
	admin_teambalance help - same as "list" (ACCESS_ALL)
	admin_teambalance on - enable all options
	admin_teambalance off - disable all options
	admin_teambalance save - save all settings to vault.ini
	admin_teambalance load - load all settings from vault.ini
 
	// team selection control
	admin_teambalance limitjoin on|off (default: on) - team join control
	admin_teambalance limitafter <value> (default: 0) - rounds after which teams limiting begins
	admin_teambalance limitmin <value> (default: 0) - minimum players for team limiting
	admin_teambalance maxsize <value> (default: 10) - maximum team size per team
	admin_teambalance autorounds <value> (default: 3) - initial rounds without free team choice
	admin_teambalance maxdiff <value> (default: 2) - maximum accepted team size difference
	admin_teambalance wtjauto <value> (default: 2) - WTJ tries for auto-join
	admin_teambalance wtjkick <value> (default: 3) - WTJ tries for kick
	admin_teambalance kick on|off (default: on) - WTJ kicking
	admin_teambalance savewtj on|off (default: off) - wtj.log writing
 
	// team balancing actions
	admin_teambalance switch on|off (default: on) - team switching and transfers
	admin_teambalance switchafter <value> (default: 0) - rounds after which switching begins
	admin_teambalance switchmin <value> (default: 3) - minimum players on map for switching
	admin_teambalance switchfreq <value> (default: 1) - maximum team switching frequency
	admin_teambalance playerfreq <value> (default: 3) - maximum player switching frequency
	admin_teambalance forceswitch <value> (default: 3) - forced switch delay (switching alive, if neccessary)
	admin_teambalance deadonly on|off (default: on) - switch dead only or alive, too
 
	// messages
	admin_teambalance tellwtj on|off (default: on) - tell about wtj tries
	admin_teambalance announce on|off (default: on) - announcements
	admin_teambalance sayok on|off (default: on) - "no action required" message
	admin_teambalance typesay on|off (default: on) - globally switch use of typesay
 
	// team strength limits
	admin_teambalance maxstreak <value> (default: 2) - maximum accepted team win streak
	admin_teambalance maxscore <value> (default: 2) - maximum accepted team score difference
	admin_teambalance minrating <value> (default: 1.5) - stronger team, if rating >= minrating
	admin_teambalance maxrating <value> (default: 2.0) - way stronger team, if rating >= maxrating
	admin_teambalance superrating <value> (default: 3.0) - overwhelming team strength, if rating >= superrating
	 (don't play with these, if you don't fully understand, what they mean)
 
	Configuration Using "vault.ini"
	-------------------------------
	PTB options can be set by manipulating the "option_..." variables at
	the top of the code, or by setting variables in AdminMod's vault.ini
	file. If you want to do the latter, these are the default settings
	as they should appear in the file (each option should stand at the
	beginning of a new line). Look at the comments besides the corresponding
	"options_..." code to find out what the options do. Atlernatively you can
	type "admin_teambalance status" in the console of a running CS server to get some
	info on the settings.
 
	// team selection control
	PTB_LIMITJOIN    on
	PTB_LIMITAFTER   0
	PTB_LIMITMIN     0
	PTB_MAXSIZE      10
	PTB_MAXDIFF      2
	PTB_AUTOROUNDS   3
	PTB_WTJAUTO      2
	PTB WTJKICK      3
	PTB_KICK         on
	PTB_SAVEWTJ      off
 
	// team balancing actions
	PTB_SWITCH       on
	PTB_SWITCHAFTER  0
	PTB_SWITCHMIN    3
	PTB_SWITCHFREQ   1
	PTB_PLAYERFREQ   3
	PTB_FORCESWITCH  3
	PTB_DEADONLY     on
 
	// messages
	PTB_TELLWTJ      on
	PTB_ANNOUNCE     on
	PTB_SAYOK        on
	PTB_TYPESAY      on
 
	// team strength limits
	PTB_MAXSTREAK    2
	PTB_MAXSCORE     2
	PTB_MINRATING    1.5
	PTB_MAXRATING    2.0
	PTB_SUPERRATING  3.0
 
*/
 
#pragma dynamic 32768
 
#include <core>
#include <console>
#include <string>
#include <admin>
#include <adminlib>
#include <fixed>
 
new STRING_VERSION[MAX_DATA_LENGTH] = "1.0Z";
 
#define ACCESS_BALANCE 32
#define ACCESS_CONSOLE 131072
 
#define TS 1
#define CTS 2
#define AUTO_TEAM 5
 
// defaults
new MIN_RATING[10] = "1.5";
new MAX_RATING[10] = "2.0";
new SUPER_RATING[10] = "3.0";
 
// options
 
// team selection control
new PTB_LIMITJOIN = 1; // set limits on team joining
new PTB_LIMITAFTER = 2; // number of rounds after which teams limiting begins
new PTB_LIMITMIN = 6; // number of minimum players on map for team limiting
new PTB_MAXSIZE = 16; // maximum team size per team
new PTB_MAXDIFF = 2; // maximum team size difference
new PTB_AUTOROUNDS = 2; // number of rounds into match, which allow autojoin only
new PTB_WTJAUTO = 3; // wtj tries needed to become autojoined
new PTB_WTJKICK = 5; // wtj tries needed to become kicked
new PTB_KICK = 1; // kick for wtj counts
new PTB_SAVEWTJ = 0; // save wtjs to wtj.txt
 
// team balancing actions
new PTB_SWITCH = 1; // switch/transfer players
new PTB_SWITCHAFTER = 1; // number of rounds after which switching begins
new PTB_SWITCHMIN = 3; // number of minimum players on map for switching
new PTB_SWITCHFREQ = 1; // relative next possible switch round
new PTB_PLAYERFREQ = 3; // relative next possible switch round for player
new PTB_FORCESWITCH = 2; // number of tries after which PTB switches alive, if neccessary
new PTB_DEADONLY = 1; // switch dead only
 
// messages
new PTB_TELLWTJ = 0; // tell about wtj tries
new PTB_ANNOUNCE = 0; // announce team status at beginning of round
new PTB_SAYOK = 0; // announce team status, if teams are alright
new PTB_TYPESAY = 0; // use typesay
 
// team strength limits
new PTB_MAXSTREAK = 2; // max allowed team win streak
new PTB_MAXSCORE = 2; // max allowed team score difference
new fixed:PTB_MINRATING = 1.5; // minimum critical team rating
new fixed:PTB_MAXRATING = 2.0; // maximum critical team rating
new fixed:PTB_SUPERRATING = 3.0; // super critical team rating
 
new sortedTeams[3][MAX_PLAYERS];
 
new validTargetCounts[3];
// sorted player indices are 0-based
new sortedValidTargets[3][MAX_PLAYERS];
 
new kills[MAX_PLAYERS] = { 0, ... };
new deaths[MAX_PLAYERS] = { 0, ... };
 
new teamKills[3] = { 0, ... };
new teamDeaths[3] = { 0, ... };
new teamScores[3] = { 0, ... };
new winStreaks[3] = { 0, ... };
 
new wtConditions[3];
new winnerTeam = 0;
new loserTeam = 0;
 
new roundCounter = 0;
new betweenRounds = true;
 
new fixed:ctKD;
new fixed:tKD;
new fixed:ctStrength;
new fixed:tStrength;
new fixed:ctRating;
new fixed:tRating;
 
// player arrays are 1-based, there is no player 0
new isChoosingTeam[MAX_PLAYERS];
new isBeingTransfered[MAX_PLAYERS];
new playerTeam[MAX_PLAYERS];
new lastRoundSwitched[MAX_PLAYERS];
new wtjCount[MAX_PLAYERS];
new teamCounts[3] = { 0, ... };
 
new lastSwitchRound = 0;
new couldNotSwitchCounter = 0;
 
new lastTeamBalanceCheck[MAX_TEXT_LENGTH];
 
fixed:fdivWorkaround (fixed:nom, fixed:denom) {
	// return nom, if denom == 0
	if (denom == fixed(0)) return nom;
 
	// handle negative result (no idea, if negativ results are reliable ...)
	if ((nom >= fixed(0)) && (denom < fixed(0))) return fdiv(nom, denom);
	if ((nom < fixed(0)) && (denom > fixed(0))) return fdiv(nom, denom);
 
	// workaround for negative results on dividing 2 positive or 2 negative numbers
	nom = fabs(nom);
	denom = fabs(denom);
	new fixed:result = nom;
	new MAX_TRIES = 20;
	new tries = 0;
	while (tries < MAX_TRIES) {
	    result = fdiv(nom, denom);
	    if (fmul(result, denom) == nom) break;
	    if (result >= fixed(0)) break;
	    ++tries;
	}
	return (result);
}
 
 
doTypesay (string[], duration, r, g, b) {
	if (!PTB_TYPESAY) return;
	typesay(string, duration, r, g, b);
}
 
 
transferPlayer (player) {
	//say("transferPlayer");
 
	new Name[MAX_NAME_LENGTH];
	playerinfo(player, Name, MAX_NAME_LENGTH);
 
	isBeingTransfered[player] = 1;
 
	//say("Transferring Player:");
	//say(Name);
 
	if (playerTeam[player] == TS) {
	    execclient(Name,"chooseteam;wait;wait;wait;wait;menuselect 2;slot2;wait;wait;menuselect 0;slot10");
	}
	else {
	    execclient(Name,"chooseteam;wait;wait;wait;wait;menuselect 1;slot1;wait;wait;menuselect 0;slot10");
	}
 
	execclient(Name, "wait; wait; wait; slot6, wait; wait; wait; slot6");
	execclient(Name, "wait; wait; wait; slot6, wait; wait; wait; slot6");
	execclient(Name, "wait; wait; wait; slot6, wait; wait; wait; slot6");
	execclient(Name, "wait; wait; wait; slot6, wait; wait; wait; slot6");
	execclient(Name, "wait; wait; wait; slot6, wait; wait; wait; slot6");
}
 
 
actAtEndOfRound ()
{
	//say("act");
	if (!PTB_SWITCH) return;
 
	// skip switching for the first few rounds
	if (roundCounter <= PTB_SWITCHAFTER) return;
 
	// honor switch frequency setting
	if (roundCounter - lastSwitchRound < PTB_SWITCHFREQ) return;
 
	// skip switching for a small number of players
	if (playercount() < PTB_SWITCHMIN) return;
 
	say("Team balance: Checking teams.");
 
	checkTeamBalance();
 
	if (winnerTeam != 0) {
	    sortTeam(CTS);
	    sortTeam(TS);
 
	    if (teamCounts[winnerTeam] <= teamCounts[loserTeam]) {
                doSwitch();
	    } else if (teamCounts[loserTeam] < teamCounts[winnerTeam]) {
                doTransfer();
            }
	}
}
 
 
createValidTargets (theTeam, deadonly) {
        new Name[MAX_NAME_LENGTH];
        new WonId;
        new UserId;
        new team;
        new dead;
 
        //say("createValidTargets");
        new n = 0;
        for (new i = 0; i < teamCounts[theTeam]; ++i) {
            if (0 == playerinfo(sortedTeams[theTeam][i], Name, MAX_NAME_LENGTH, WonId, UserId, team, dead)) {
                say("No playerinfo ...");
            }
            //snprintf(Name, MAX_NAME_LENGTH, "Index: %d, sortedTeamIndex: %d, dead: %d", i , sortedTeams[theTeam][i], dead);
            //say(Name);
            if (dead == 0 && deadonly) {
                //say("player alive");
                continue;
            }
            if ((roundCounter == lastRoundSwitched[sortedTeams[theTeam][i]])){
                //say("Player switched this round...")
                continue;
            }
            if ((roundCounter - lastRoundSwitched[sortedTeams[theTeam][i]] > PTB_PLAYERFREQ)) {
                //say("Player switched not in this round...")
                sortedValidTargets[theTeam][n] = sortedTeams[theTeam][i];
                ++n;
                continue;
            }
            //say("player dead and not switched this round");
            sortedValidTargets[theTeam][n] = sortedTeams[theTeam][i];
            ++n;
        }
        validTargetCounts[theTeam] = n;
}
 
 
sortTeam (theTeam) {
	//say("sortTeam");
 
	// create list of players
	new n = 0;
	for (new i=1; i<MAX_PLAYERS; ++i) {
	    if (playerTeam[i] != theTeam) continue;
	    sortedTeams[theTeam][n] = i;
	    ++n;
	}
 
	// do a selection sort
	new count = teamCounts[theTeam];
	for (new i=count-1; i>0; --i) {
	    for (new k=i-1; k>=0; --k) {
		// compare players
		if ((kills[sortedTeams[theTeam][k]] < kills[sortedTeams[theTeam][i]]) ||
		    ((kills[sortedTeams[theTeam][k]] == kills[sortedTeams[theTeam][i]])
		     && (deaths[sortedTeams[theTeam][k]] > deaths[sortedTeams[theTeam][i]]))) {
		    // swap
		    new t;
		    t = sortedTeams[theTeam][k];
		    sortedTeams[theTeam][k] = sortedTeams[theTeam][i];
		    sortedTeams[theTeam][i] = t;
		}
	    }
	}
}
 
 
fixed:fabs (fixed:n) {
	if (n >= fixed(0)) return n;
	return (-n);
}
 
 
fixed:score (team, toBeAdded, toBeRemoved) {
	new fixed:sumKD = fixed(0);
 
	for (new player=1; player<MAX_PLAYERS; ++player) {
	    if (((playerTeam[player] != team) && (player != toBeAdded)) || (player == toBeRemoved)) continue;
	    sumKD += fdivWorkaround(fixed(kills[player]), fixed(deaths[player]));
	}
 
	new fixed:strength = fixed(teamCounts[team]);
	if (sumKD > fixed(0)) strength = fmul(strength, sumKD);
 
	return (strength);
}
 
 
doSwitch () {
	//say("doSwitch");
	new text[MAX_TEXT_LENGTH];
 
	//displayStatistics(true);
 
	// don't switch, if at least one team is empty
	if ((teamCounts[winnerTeam] == 0) || (teamCounts[loserTeam] == 0)) {
	    text = "Team balance: Can't switch players, need players in each team.";
	    doTypesay(text, 5, 10, 255, 10);
	    say(text);
	    return;
	}
 
	// don't switch, if winner is alone (RULER!!!)
	if (teamCounts[winnerTeam] == 1) {
	    text = "Team balance: Won't switch players, best player makes the winning team.";
	    doTypesay(text, 5, 10, 255, 10);
	    say(text);
	    return;
	}
 
	// don't switch, if both teams are full
	if ((teamCounts[winnerTeam] == PTB_MAXSIZE) && (teamCounts[loserTeam] == PTB_MAXSIZE)) {
	    new nReserved = getvar("reserve_slots");
	    if (nReserved == 0) {
		text = "Team balance: Can't switch players, both teams are full.";
		doTypesay(text, 5, 10, 255, 10);
		say(text);
	    	return;
	    }
	}
 
	if (!PTB_DEADONLY || (couldNotSwitchCounter > PTB_FORCESWITCH)) {
	    // choose from random top or bottom x
	    createValidTargets(winnerTeam, 0);
	    createValidTargets(loserTeam, 0);
 
	    if ((validTargetCounts[winnerTeam] == 0) || (validTargetCounts[loserTeam] == 0)) {
		strcpy(text, "Team balance: Can't switch players, need valid target in each team.", MAX_DATA_LENGTH);
		doTypesay(text, 5, 10, 255, 10);
		say(text);
		++couldNotSwitchCounter;
		return;
	    }
	} else {
	    //say("switch dead");
	    createValidTargets(winnerTeam, 1);
	    createValidTargets(loserTeam, 1);
 
	    if ((validTargetCounts[winnerTeam] == 0) || (validTargetCounts[loserTeam] == 0)) {
		++couldNotSwitchCounter;
		if (couldNotSwitchCounter > PTB_FORCESWITCH) {
		    say("Team balance: Couldn't switch dead, switching alive.");
		    doSwitch();
		    return;
		}
		strcpy(text, "Team balance: Can't switch players, need valid target in each team.", MAX_DATA_LENGTH);
		doTypesay(text, 5, 10, 255, 10);
		say(text);
		return;
	    }
	}
 
	// Now search through the possible 1 to 1 swaps to equalize the strength as much as possible
	new fixed:closestScore = fabs(score(winnerTeam, -1, -1) - score(loserTeam, -1, -1));
	new winner = 0;
	new loser = 0;
 
	for (new w=0; w<validTargetCounts[winnerTeam]; ++w) {
	    new toLoser = sortedValidTargets[winnerTeam][w];
	    for (new l=0; l<validTargetCounts[loserTeam]; ++l) {
		new toWinner = sortedValidTargets[loserTeam][l];
		new fixed:myScore = fabs(score(winnerTeam, toWinner, toLoser) - score(loserTeam, toLoser, toWinner));
		if (myScore < closestScore) {
		    closestScore = myScore;
		    winner = toLoser;
		    loser = toWinner;
		}
	    }
	}
 
	if (winner == 0 && loser == 0) {
	    strcpy(text, "Team balance: No switch would improve team balancing.", MAX_DATA_LENGTH);
	    doTypesay(text, 5, 10, 255, 10);
	    say(text);
	    return;
	}
 
	couldNotSwitchCounter = 0;
	lastSwitchRound = roundCounter;
 
	new winnerName[MAX_NAME_LENGTH];
	new loserName[MAX_NAME_LENGTH];
 
	playerinfo(winner, winnerName, MAX_NAME_LENGTH);
	playerinfo(loser, loserName, MAX_NAME_LENGTH);
 
	// if one team is full, first move the the player from the full team ...
	if (teamCounts[winnerTeam] == PTB_MAXSIZE) {
	    new nReserved = getvar("reserve_slots");
	    if (nReserved > 0) {
	    	new NresBuf[8];
	    	snprintf(NresBuf, 8, "%i", nReserved-1);
	    	//execute_command(User, Command, "reserve_slots", NresBuf);
		set_serverinfo("reserve_slots", NresBuf);
 
		transferPlayer(winner);
		transferPlayer(loser);
 
	    	snprintf(NresBuf, 8, "%i", nReserved);
	    	//execute_command(User, Command, "reserve_slots", NresBuf);
		set_serverinfo("reserve_slots", NresBuf);
	    } else {
		transferPlayer(winner);
		transferPlayer(loser);
	    }
	} else {
	    transferPlayer(loser);
	    transferPlayer(winner);
	}
 
	strcpy(text, "Team balance: Switching ", 80);
	strcat(text, winnerName, 80);
	strcat(text, " with ", 80);
	strcat(text, loserName, 80);
	strcat(text, ".", 80);
	doTypesay(text, 5, 10, 255, 10);
	say(text);
}
 
 
doTransfer () {
	//say("doTransfer");
	new text[MAX_TEXT_LENGTH];
 
	if (teamCounts[winnerTeam] == 0) return;
 
	//displayStatistics(true);
 
	if (teamCounts[loserTeam] == PTB_MAXSIZE) {
	    strcpy(text, "Team balance: Can't transfer player, losing team is full.", MAX_DATA_LENGTH);
	    doTypesay(text, 5, 10, 255, 10);
	    say(text);
	    return;
	}
 
	if (!PTB_DEADONLY || couldNotSwitchCounter > PTB_FORCESWITCH) {
	    createValidTargets(winnerTeam, 0);
 
	    if (validTargetCounts[winnerTeam] == 0) {
		strcpy(text, "Team balance: Can't transfer player, no valid target in winning team.", MAX_DATA_LENGTH);
		doTypesay(text, 5, 10, 255, 10);
		say(text);
		++couldNotSwitchCounter;
		return;
	    }
	} else {
	    //say("switch dead");
	    createValidTargets(winnerTeam, 1);
	    if (validTargetCounts[winnerTeam] == 0) {
		++couldNotSwitchCounter;
		if (couldNotSwitchCounter > PTB_FORCESWITCH) {
		    say("Team balance: Couldn't transfer dead, transferring alive.");
		    doTransfer();
		    return;
		}
		strcpy(text, "Team balance: Can't transfer player, no valid target in winning team.", MAX_DATA_LENGTH);
		doTypesay(text, 5, 10, 255, 10);
		say(text);
		return;
	    }
	}
 
	new fixed:closestScore = fabs(score(winnerTeam, -1, -1) - score(loserTeam, -1, -1));
	new winner = -1;
 
	for (new w = 0; w < validTargetCounts[winnerTeam]; ++w) {
	    new toLoser = sortedValidTargets[winnerTeam][w];
	    new fixed:myScore = fabs(score(winnerTeam, -1, toLoser) - score(loserTeam, toLoser, -1));
	    if (myScore < closestScore) {
		closestScore = myScore;
		winner = toLoser;
	    }
	}
 
	if (winner == -1) {
	    strcpy(text, "Team balance: No transfer would improve team balancing.", MAX_DATA_LENGTH);
	    doTypesay(text, 5, 10, 255, 10);
	    say(text);
	    return;
	}
 
	couldNotSwitchCounter = 0;
 
	new winnerName[MAX_NAME_LENGTH];
 
	playerinfo(winner, winnerName, MAX_NAME_LENGTH);
 
	transferPlayer(winner);
 
	strcpy(text, "Team balance: Transfering ", 80);
	strcat(text, winnerName, 80);
	strcat(text, " to the ", 80);
	if (winnerTeam == CTS) strcat(text, "Ts", 80);
	else if (winnerTeam == TS) strcat(text, "CTs", 80);
	strcat(text, ".", 80);
	doTypesay(text, 5, 10, 255, 10);
	say(text);
}
 
 
checkTeamBalance () {
	//say("Checking team balance ...");
 
	servertime(lastTeamBalanceCheck, MAX_TEXT_LENGTH, "none");
 
	calcTeamScores();
 
	//ctStrength = fixed(teamCounts[CTS]);
	// workaround for match start team balancing
	// this is making only team size count,
	// as long as there is no single kill
	//if (ctKD != fixed(0)) ctStrength = fmul(ctKD, ctStrength);
 
	// replacing old strength calculation
	ctStrength = score(CTS, -1, -1);
 
	//tStrength = fixed(teamCounts[TS]);
	// workaround for match start team balancing
	// this is making only team size count,
	// as long as there is no single kill
	//if (tKD != fixed(0)) tStrength = fmul(tKD, tStrength);
 
	// replacing old strength calculation
	tStrength = score(TS, -1, -1);
 
	ctRating = fdivWorkaround(ctStrength, tStrength);
	tRating = fdivWorkaround(tStrength, ctStrength);
 
	wtConditions[TS] = 0;
	wtConditions[CTS] = 0;
 
	//new buffer[10];
 
	// compare scores for unequal rating scores
	if ((teamScores[TS] - teamScores[CTS] > PTB_MAXSCORE) && (tRating >= PTB_MINRATING)) {
	    wtConditions[TS] = wtConditions[TS] + 1;
	}
	if ((teamScores[CTS] - teamScores[TS] > PTB_MAXSCORE) && (ctRating >= PTB_MINRATING)) {
	    wtConditions[CTS] = wtConditions[CTS] + 1;
	}
 
	//say("// check streaks for unequal rating scores");
	// check streaks for unequal rating scores
	if ((winStreaks[TS] > PTB_MAXSTREAK) && (tRating >= PTB_MINRATING)) {
	    wtConditions[TS] = wtConditions[TS] + 1;
	}
	if ((winStreaks[CTS] > PTB_MAXSTREAK) && (ctRating >= PTB_MINRATING)) {
	    wtConditions[CTS] = wtConditions[CTS] + 1;
	}
 
	//say("// check ratings");
	// check ratings
	if (tRating >= PTB_MAXRATING) {
	    wtConditions[TS] = wtConditions[TS] + 1;
	}
	if (ctRating >= PTB_MAXRATING) {
	    wtConditions[CTS] = wtConditions[CTS] + 1;
	}
 
	//say("// check super ratings");
	// check ratings
	if (tRating >= PTB_SUPERRATING) {
	    wtConditions[TS] = wtConditions[TS] + 1;
	}
	if (ctRating >= PTB_SUPERRATING) {
	    wtConditions[CTS] = wtConditions[CTS] + 1;
	}
 
	//say("// check team sizes for unequal ratings");
	// check team sizes for unequal ratings
	if ((teamCounts[TS] > teamCounts[CTS]) && (tRating >= PTB_MINRATING)) {
	    wtConditions[TS] = wtConditions[TS] + 1;
	}
	if ((teamCounts[CTS] > teamCounts[TS]) && (ctRating >= PTB_MINRATING)) {
	    wtConditions[CTS] = wtConditions[CTS] + 1; 
	}
 
	//say("// check conditions");
	// check conditions
	if (wtConditions[TS] >= 2) {
	    //say("TERRORISTS are the winning team ...");
	    winnerTeam = TS;
	    loserTeam = CTS;
	} else if (wtConditions[CTS] >= 2) {
	    //say("COUNTER-TERRORISTS are the winning team ...");
	    winnerTeam = CTS;
	    loserTeam = TS;
	} else {
	    winnerTeam = 0;
	    loserTeam = 0;
	    //if (wtConditions[CTS] > 0) say("COUNTER-TERRORIST team advantage ...");
	    //else if (wtConditions[TS] > 0) say("TERRORIST team advantage ...");
	    //else say("No winning team, no team advantage ...");
	}
}
 
 
public ChoosingTeam (HLCommand, HLData, HLUserName, UserIndex) {
	//say("ChoosingTeam");
 
	isChoosingTeam[UserIndex] = 1;
 
	return PLUGIN_CONTINUE;
}
 
 
public NotChoosingTeam (HLCommand, HLData, HLUserName, UserIndex) {
	//say("NotChoosingTeam");
 
	isChoosingTeam[UserIndex] = 0;
 
	return PLUGIN_CONTINUE;
}
 
 
manageWtjFile (UserIndex) {
	if (!PTB_SAVEWTJ) return;
 
	//log("Trying to write wtj.log ....");
 
	if (wtjCount[UserIndex] != 4) return;
 
	//log("wtj.log should be written to now ....");
 
	//new debug[MAX_DATA_LENGTH];
	//snprintf(debug, MAX_DATA_LENGTH, "PTB manageWtjFile");
	//log(debug);
 
	new Time[MAX_DATA_LENGTH];
	servertime(Time, MAX_DATA_LENGTH);
 
	new Map[MAX_DATA_LENGTH];
	currentmap(Map, MAX_DATA_LENGTH);
 
	new Name[MAX_NAME_LENGTH];
	new UserId;
	new WonId;
	playerinfo(UserIndex, Name, MAX_NAME_LENGTH, UserId, WonId);
 
	new text[MAX_TEXT_LENGTH];
	snprintf(text, MAX_TEXT_LENGTH, "%s %s <%d> %s", Time, Name, WonId, Map);
 
	writefile("wtj.log", text);
}
 
 
public MenuSelect (HLCommand, HLData, HLUserName, UserIndex) {
	//say("MenuSelect");
 
	if (!isChoosingTeam[UserIndex]) return PLUGIN_CONTINUE;
 
	isChoosingTeam[UserIndex] = 0;
 
	//if (UserIndex < 1) return PLUGIN_CONTINUE;
	if (!PTB_LIMITJOIN) return PLUGIN_CONTINUE;
 
	// skip limiting for a few rounds into the map
	if (roundCounter <= PTB_LIMITAFTER) return PLUGIN_CONTINUE;
 
	// skip limiting for a small number of players
	if (playercount() < PTB_LIMITMIN) return PLUGIN_CONTINUE;
 
	if (isBeingTransfered[UserIndex]) {
	    //say("TRANSFER");
	    isBeingTransfered[UserIndex] = 0;
	    return PLUGIN_CONTINUE;
	} else {
	    //say("NO TRANSFER");
	}
 
	new Data[MAX_DATA_LENGTH];
	new iNewTeam;
 
	convert_string(HLData, Data, MAX_DATA_LENGTH);
	iNewTeam = strtonum(Data);
 
	// disallow free team choices in the first rounds of a map
	if (roundCounter <= PTB_AUTOROUNDS) iNewTeam = 5;
 
	new iOldTeam = playerTeam[UserIndex];
 
	new Name[MAX_NAME_LENGTH];
	playerinfo(UserIndex, Name, MAX_NAME_LENGTH);
 
	// prevent unwanted rejoining of the same team ...
	if (iNewTeam == playerTeam[UserIndex]) {
	    //say("Preventing rejoining of the same team.");
	    return PLUGIN_HANDLED;
	}
 
	checkTeamBalance();
 
	//displayStatistics(true);
 
	// check if player is trying to change teams
	if ((iNewTeam == CTS || iNewTeam == TS) && (iOldTeam == CTS || iOldTeam == TS)) {
	    //say("Changing team directly.");
	    new opposingTeam = iNewTeam == CTS ? TS : CTS;
	    if (teamCounts[iNewTeam] != 0
		    && teamCounts[opposingTeam] < PTB_MAXSIZE
		    && ((winnerTeam != 0 && iNewTeam == winnerTeam)
			|| (teamCounts[iNewTeam] >= teamCounts[opposingTeam]) ) ) {
		// player is wtjing
		new text[MAX_TEXT_LENGTH];
		wtjCount[UserIndex] += 1;
		if (wtjCount[UserIndex] >= PTB_WTJKICK && PTB_KICK) {
		    snprintf(text, 80, "Team balance: Kicking %s for a WTJ count of %d).", Name, wtjCount[UserIndex]);
		    doTypesay(text, 5, 10, 255, 10);
		    say(text);
		    kick(Name);
		    return PLUGIN_HANDLED;
		}
		if (PTB_TELLWTJ) {
		    if (iNewTeam == CTS) {
			snprintf(text, 80, "Team balance: The CTs are strong enough, %s (WTJ count: %d).", Name, wtjCount[UserIndex]);
			doTypesay(text, 5, 10, 10, 255);
		    } else {
			snprintf(text, 80, "Team balance: The Ts are strong enough, %s (WTJ count: %d).", Name, wtjCount[UserIndex]);
			doTypesay(text, 5, 255, 10, 10);
		    }
		    say(text);
		}
		return PLUGIN_HANDLED;
	    }
 
	    // check for maximum team size
	    if (teamCounts[iNewTeam] + 1 > PTB_MAXSIZE) {
		selfmessage("Team balance: Maximum team size prohibits team change.");
		return PLUGIN_HANDLED;
	    }
 
	    // check team size difference limits
	    if ((teamCounts[iNewTeam] + 1) - (teamCounts[opposingTeam] - 1) > PTB_MAXDIFF) {
		selfmessage("Team balance: Maximum team size difference prohibits team change.");
		return PLUGIN_HANDLED;
	    }
	} else if (iNewTeam == CTS || iNewTeam == TS) {
	    //say("Joining team directly.");
	    new opposingTeam = iNewTeam == CTS ? TS : CTS;
	    if (teamCounts[iNewTeam] != 0
		    && teamCounts[opposingTeam] < PTB_MAXSIZE
		    && (
			    (winnerTeam != 0 && iNewTeam == winnerTeam) // && teamCounts[winnerTeam] >= teamCounts[opposingTeam])
			    || (winnerTeam == 0 && teamCounts[iNewTeam] > teamCounts[opposingTeam]) ) ) {
		new text[MAX_TEXT_LENGTH];
		wtjCount[UserIndex] += 1;
		if (wtjCount[UserIndex] >= PTB_WTJKICK && PTB_KICK) {
			snprintf(text, 80, "Team balance: Kicking %s for a WTJ count of %d).", Name, wtjCount[UserIndex]);
			doTypesay(text, 5, 10, 255, 10);
			say(text);
			kick(Name);
			return PLUGIN_HANDLED;
		}
		if (iNewTeam == CTS) {
		    if (wtjCount[UserIndex] > PTB_WTJAUTO) {
			manageWtjFile(UserIndex);
			snprintf(text, 80, "Team balance: Forcing %s to the Ts (WTJ count: %d).", Name, wtjCount[UserIndex]);
			execclient(Name, "menuselect 1");
			doTypesay(text, 5, 10, 255, 10);
			say(text);
		    } else {
			if (PTB_TELLWTJ) {
			    snprintf(text, 80, "Teambalance: The CTs are strong enough, %s (WTJ count: %d).", Name, wtjCount[UserIndex]);
			    doTypesay(text, 5, 10, 10, 255);
			    say(text);
			}
		    }
		} else {
		    if (wtjCount[UserIndex] >= PTB_WTJAUTO) {
			manageWtjFile(UserIndex);
			snprintf(text, 80, "Teambalance: Forcing %s to the CTs (WTJ count: %d).", Name, wtjCount[UserIndex]);
			execclient(Name, "menuselect 2");
			doTypesay(text, 5, 10, 255, 10);
			say(text);
		    } else {
			if (PTB_TELLWTJ) {
			    snprintf(text, 80, "Teambalance: The Ts are strong enough, %s (WTJ count: %d).", Name, wtjCount[UserIndex]);
			    doTypesay(text, 5, 255, 10, 10);
			    say(text);
			}
		    }
		}
		return PLUGIN_HANDLED;
	    }
 
	    // check for maximum team size
	    if (teamCounts[iNewTeam] + 1 > PTB_MAXSIZE) {
		    selfmessage("Teambalance: Maximum team size prohibits team join.");
		    return PLUGIN_HANDLED;
	    }
 
	    // check team size difference limits
	    if ((teamCounts[iNewTeam] + 1) - (teamCounts[opposingTeam]) > PTB_MAXDIFF) {
		    selfmessage("Teambalance: Maximum team size difference prohibits team join.");
		    return PLUGIN_HANDLED;
	    }
 
	} else if (iNewTeam == AUTO_TEAM && (iOldTeam == CTS || iOldTeam == TS)) {
	    //say("Changing team automatically.");
 
	    new opposingTeam = iOldTeam == CTS ? TS : CTS;
	    if ((teamCounts[opposingTeam] != 0)
		&& ((opposingTeam == PTB_MAXSIZE)
			|| ((loserTeam != 0) && (iOldTeam == loserTeam))
			|| ((loserTeam == 0) && (teamCounts[iOldTeam] <= teamCounts[opposingTeam]))
			|| ((teamCounts[opposingTeam] + 1) - (teamCounts[iOldTeam] - 1) > PTB_MAXDIFF) ) ) {
		//say("Aborting automatic team change.");
 
		// close menu
		return PLUGIN_HANDLED;
	    }
 
	    iNewTeam = opposingTeam;
 
	    // check for maximum team size
	    if (teamCounts[iNewTeam]+1 > PTB_MAXSIZE) {
		selfmessage("Teambalance: Maximum team size prohibits team change.");
		return PLUGIN_HANDLED;
	    }
 
	    if (iNewTeam == CTS) {
                execclient(Name, "menuselect 2");
	    } else {
                execclient(Name, "menuselect 1");
            }
 
	    return PLUGIN_HANDLED;
 
	} else if (iNewTeam == AUTO_TEAM) {
	    //say("Joining team automatically.");
 
	    // this version prefers the losing team (but still honors PTB_MAXDIFF)
	    if (teamCounts[CTS] == PTB_MAXSIZE) iNewTeam = TS;
	    else if (teamCounts[TS] == PTB_MAXSIZE) iNewTeam = CTS;
	    else if ((teamCounts[CTS] + 1) - (teamCounts[TS]) > PTB_MAXDIFF) iNewTeam = TS;
	    else if ((teamCounts[TS] + 1) - (teamCounts[CTS]) > PTB_MAXDIFF) iNewTeam = CTS;
	    else if (winnerTeam != 0) iNewTeam = loserTeam;
	    else if (teamCounts[CTS] < teamCounts[TS]) iNewTeam = CTS;
	    else if (teamCounts[TS] < teamCounts[CTS]) iNewTeam = TS;
	    // both teams have same size ...
	    else iNewTeam = random(2) + 1;
 
	    // check for maximum team size
	    if (teamCounts[iNewTeam] + 1 > PTB_MAXSIZE) {
		selfmessage("Team balance: Maximum team size prohibits team join.");
		return PLUGIN_HANDLED;
	    }
 
	    if (iNewTeam == CTS) {
                execclient(Name, "menuselect 2");
	    } else {
                execclient(Name, "menuselect 1");
            }
 
	    return PLUGIN_HANDLED;
	}
 
	//say("Allow team join ...");
 
	return PLUGIN_CONTINUE;
}
 
 
public ptb_teamscore (HLCommand, HLData, HLUserName, UserIndex)
{
	new params[MAX_DATA_LENGTH];
	new team[MAX_DATA_LENGTH];
	new myscore[8];
	new target[3];
	convert_string(HLData, params, MAX_DATA_LENGTH);
	strsplit(params, " ",target,3,team, MAX_DATA_LENGTH,myscore,8);
 
	if (streq(team,"CT")) {
	    teamScores[CTS] = strtonum(myscore);
	} else if (streq(team,"TERRORIST")) {
	    teamScores[TS] = strtonum(myscore);
	}
}
 
 
public ptb_teamaction (HLCommand, HLData, HLUserName, UserIndex)
{
	//say("ptb_teamaction");
 
	new params[MAX_DATA_LENGTH];
	new type[3];
	new action[MAX_DATA_LENGTH];
	new target[3];
 
	convert_string(HLData, params, MAX_DATA_LENGTH);
	strsplit(params, " ",target,3,type, 3,action,MAX_DATA_LENGTH);
 
	// count won rounds in a row
	if (!betweenRounds) {
	    // prevent "bomb explodes after ts win" situations
	    // from producing 2 wins in the same round
	    if (streq(action, "%!MRAD_ctw")) {
		if (winStreaks[CTS] < 0) {
		    // reset counters
		    winStreaks[CTS] = 1;
		    winStreaks[TS] = -1;
		} else {
		    winStreaks[CTS] += 1;
		    winStreaks[TS] -= 1;
		}
	    } else if (streq(action, "%!MRAD_terw")) {
		if (winStreaks[TS] < 0) {
		    // reset counters
		    winStreaks[CTS] = -1;
		    winStreaks[TS] = 1;
		} else {
		    winStreaks[CTS] -= 1;
		    winStreaks[TS] += 1;
		}
	    }
 
	    actAtEndOfRound();
	}
 
	// clear flags
	betweenRounds = true;
 
	return PLUGIN_CONTINUE;
}
 
 
public ptb_worldaction (HLCommand, HLData, HLUserName, UserIndex) {
	//log("Team Balance: ptb_worldaction");
	//log(Data);
 
	//if (streq(Data, "Round_Start")) {
	++roundCounter;
	betweenRounds = false;
	//set_timer("announceStatus", 15, 1);
	announceStatus();
	//}
 
	return PLUGIN_CONTINUE;
}
 
 
public ptb_teamselection (HLCommand, HLData, HLUserName, UserIndex) {
	//say("ptb_teamselection");
 
	//displayStatistics(true);
 
	new data[MAX_DATA_LENGTH];
	new target[3];
	new sIndex[4];
	new sTeam[MAX_DATA_LENGTH];
	new i;
	new team;
 
	convert_string(HLData, data, MAX_DATA_LENGTH);
 
	strsplit(data, " ",target, 3 , sIndex, 4, sTeam,MAX_DATA_LENGTH);
 
	//log("Team Balance: ptb_teamselection");
	//log(data);
 
	//strsplit(data, " ", sIndex, MAX_DATA_LENGTH, sTeam, MAX_DATA_LENGTH);
 
	//new debug[MAX_DATA_LENGTH];
	//snprintf(debug, MAX_DATA_LENGTH, "Team Balance: ptb_teamselection: %s", data);
	//log(debug);
 
	if (streq(sTeam, "TERRORIST")) {
            team = TS;
	} else if (streq(sTeam, "CT")) {
            team = CTS;
	} else {
            team = 0;
        }
 
	//if (team == 0) say("Team Balance: ptb_teamselection: Team is 0.");
 
	i = strtonum(sIndex);
 
	if ((playerTeam[i] == TS) || (playerTeam[i] == CTS)) teamCounts[playerTeam[i]] -= 1;
	if ((team == TS) || (team == CTS)) teamCounts[team] += 1;
 
	playerTeam[i] = team;
 
	lastRoundSwitched[i] = roundCounter;
 
	//say("ptb_teamselection");
	//say(data);
 
	//displayStatistics(true);
 
	return PLUGIN_CONTINUE;
}
 
 
public ptb_kill (HLCommand, HLData, HLUserName, UserIndex)
{
	//say("ptb_kill");
 
	new iWinner;
	new iLoser;
	new idWinner[3];
	new idLoser[3];
	new target[3];
	new data[MAX_DATA_LENGTH];
	new name[MAX_NAME_LENGTH];
 
	convert_string(HLData, data, MAX_DATA_LENGTH);
 
	//log("Team Balance: ptb_kill");
	//log(data);
 
	strsplit(data, " ",target,3, idWinner, 3, idLoser, 3);
 
	iWinner = strtonum(idWinner);
	iLoser = strtonum(idLoser);
 
	if (iWinner==0) {
	    return PLUGIN_CONTINUE;
	    //Happens on WorldSpawn 
	}
	if (iLoser==0) {
	    return PLUGIN_CONTINUE;
	    //shouldn*t happen 
	}
	if (!playerinfo(iWinner, name, MAX_NAME_LENGTH)) {
	    return PLUGIN_CONTINUE;
	}
 
	kills[iWinner] = kills[iWinner] + 1;
	deaths[iLoser] = deaths[iLoser] + 1;
 
	//if (playerTeam[iWinner] != CTS && playerTeam[iWinner] != TS) log("Team Balance: Kill without team!");
	//if (playerTeam[iLoser] != CTS && playerTeam[iLoser] != TS) log("Team Balance: Death without team!");
 
	return PLUGIN_CONTINUE;
}
 
 
calcTeamScores ()
{
	//say("calcTeamScores");
 
	teamDeaths[CTS] = 0;
	teamDeaths[TS] = 0;
	teamKills[CTS] = 0;
	teamKills[TS] = 0;
 
	for (new i=1; i<MAX_PLAYERS; ++i) {
	    new team = playerTeam[i];
	    if (team == CTS || team == TS) {
		teamKills[team] += kills[i];
		teamDeaths[team] += deaths[i];
	    }
	}
 
	ctKD = fdivWorkaround(fixed(teamKills[CTS]), fixed(teamDeaths[CTS]));
	tKD = fdivWorkaround(fixed(teamKills[TS]), fixed(teamDeaths[TS]));
 
	//say("calcTeamScores DONE");
}
 
 
announceStatus () {
	//say("announceStatus");
 
	if (!PTB_ANNOUNCE) return;
 
	checkTeamBalance();
 
	new text[MAX_TEXT_LENGTH];
 
	if (winnerTeam == TS) {
	    snprintf(text, MAX_TEXT_LENGTH, "Team balance: The COUNTER-TERRORIST team could use some support.");
	    doTypesay(text, 5, 10, 10, 255);
	    say("Team balance: The COUNTER-TERRORIST team could use some support.");
	} else if (winnerTeam == CTS) {
	    snprintf(text, MAX_TEXT_LENGTH, "Team balance: The TERRORIST team could use some support.");
	    doTypesay(text, 5, 255, 10, 10);
	    say("Team balance: The TERRORIST team could use some support.");
	} else if (wtConditions[TS] > wtConditions[CTS]) {
	    snprintf(text, MAX_TEXT_LENGTH, "Team balance: Observing TERRORIST team advantage.");
	    doTypesay(text, 5, 10, 10, 255);
	    say("Team balance: Observing TERRORIST team advantage.");
	} else if (wtConditions[CTS] > wtConditions[TS]) {
	    snprintf(text, MAX_TEXT_LENGTH, "Team balance: Observing COUNTER-TERRORIST team advantage.");
	    doTypesay(text, 5, 255, 10, 10);
	    say("Team balance: Observing COUNTER-TERRORIST team advantage.");
	} else if (PTB_SAYOK) {
	    snprintf(text, MAX_TEXT_LENGTH, "Team balance: Teams look fine, no action required.");
	    doTypesay(text, 5, 255, 255, 255);
	    say("Team balance: Teams look fine, no action required.");
	}
 
	//displayStatistics();
}
 
 
public admin_teambalance (HLCommand, HLData, HLUserName, UserIndex) {
	new Data[MAX_DATA_LENGTH];
	new Command[MAX_DATA_LENGTH];
	new Argument[MAX_DATA_LENGTH];
	new s[MAX_TEXT_LENGTH];
 
	convert_string(HLData, Data, MAX_NAME_LENGTH);
 
	strinit(Command);
	strinit(Argument);
 
	if (!strlen(Data)) {
	    checkTeamBalance();
	    displayStatistics();
	    return PLUGIN_HANDLED;
	} else if (streq(Data, "on") || streq(Data, "1")) {
	    if (!access(ACCESS_BALANCE, "")) {
		selfmessage("Team balance: You are not allowed to use this command.");
		return PLUGIN_HANDLED;
	    }
	    PTB_LIMITJOIN = 1;
	    PTB_SWITCH = 1;
	    PTB_ANNOUNCE = 1;
	    selfmessage("Team Balance: Enabled all PTB actions.");
	    return PLUGIN_HANDLED;
	} else if (streq(Data, "off") || streq(Data, "0")) {
	    if (!access(ACCESS_BALANCE, "")) {
		selfmessage("Team balance: You are not allowed to use this command.");
		return PLUGIN_HANDLED;
	    }
	    PTB_SWITCH = 0;
	    PTB_ANNOUNCE = 0;
	    PTB_LIMITJOIN = 0;
	    selfmessage("Team Balance: Disabled all PTB actions.");
	    return PLUGIN_HANDLED;
	} else if (streq(Data, "save")) {
	    if (!access(ACCESS_BALANCE, "")) {
		selfmessage("Team balance: You are not allowed to use this command.");
		return PLUGIN_HANDLED;
	    }
	    saveSettings();
	    selfmessage("Team balance: Saved all PTB settings to ^"vault.ini^".");
	    return PLUGIN_HANDLED;
	} else if (streq(Data, "load")) {
	    if (!access(ACCESS_BALANCE, "")) {
		selfmessage("Team balance: You are not allowed to use this command.");
		return PLUGIN_HANDLED;
	    }
	    loadSettings();
	    selfmessage("Team balance: Loaded all Team balance settings from ^"vault.ini^".");
	    return PLUGIN_HANDLED;
	} else if (streq(Data, "list") || streq(Data, "help")) {
	    selfmessage("Team balance: Available Commands:");
	    selfmessage("Team balance: Team Join Control: ^"limitjoin^", ^"limitafter^", ^"limitmin^", ^"maxsize^", ^"autorounds^",");
	    selfmessage("Team balance: ^"maxdiff^", ^"wtjauto^", ^"wtjkick^", ^"kick^", ^"savewtj^"");
	    selfmessage("Team balance: Team Balancing Actions: ^"switch^", ^"switchafter^", ^"switchmin^", ^"switchfreq^", ^"playerfreq^",");
	    selfmessage("Team balance: ^"forceswitch^", ^"deadonly^"");
	    selfmessage("Team balance: Team Strength Limits: ^"maxstreak^", ^"maxscore^", ^"minrating^", ^"maxrating^", ^"superrating^"");
	    selfmessage("Team balance: Messages: ^"tellwtj^", ^"announce^", ^"sayok^", ^"typesay^"");
	    selfmessage("Team balance: Misc: ^"^", ^"status^", ^"list^", ^"help^", ^"on^", ^"off^", ^"save^", ^"load^"");
	    selfmessage("Team balance: To view all PTB settings, type ^"admin_teambalance status^".");
	    selfmessage("Team balance: To view or change a single PTB setting, type ^"admin_teambalance <setting> <^"on^"|^"off^"|value>^".");
	    selfmessage("Team balance: For PTB statistics, simply type ^"admin_teambalance^".");
	    return PLUGIN_HANDLED;
	} else {
	    strsplit(Data, " ", Command, MAX_DATA_LENGTH, Argument, MAX_DATA_LENGTH);
	    if (strlen(Argument) && !access(ACCESS_BALANCE, "")) {
		selfmessage("Team balance: You are not allowed to use this command.");
		return PLUGIN_HANDLED;
	    }
	}
 
	// team selection control
	if (streq(Command, "status")) selfmessage("Team balance: ---------- Team Selection Control ----------");
 
	// PTB_LIMITJOIN
	if (streq(Command, "limitjoin") && strlen(Argument)) PTB_LIMITJOIN = check_param(Argument);
	if (streq(Command, "status") || streq(Command, "limitjoin")) { if (PTB_LIMITJOIN) selfmessage("Team balance: (limitjoin) WTJ prevention is ON."); else selfmessage("Team balance: (limitjoin) WTJ prevention is OFF."); }
 
	// PTB_LIMITAFTER
	if (streq(Command, "limitafter") && strlen(Argument)) {
	    PTB_MAXDIFF = strtonum(Argument);
	    if (PTB_LIMITAFTER < 0) PTB_LIMITAFTER = 0;
	}
	if (streq(Command, "status") || streq(Command, "limitafter")) {
	    snprintf(s, MAX_TEXT_LENGTH, "Team balance: (limitafter) Team limiting starts after %d round(s).", PTB_LIMITAFTER);
	    selfmessage(s);
	}
 
	// PTB_LIMITMIN
	if (streq(Command, "limitmin") && strlen(Argument)) {
	    PTB_LIMITMIN = strtonum(Argument);
	    if (PTB_LIMITMIN < 0) PTB_LIMITMIN = 0;
	}
	if (streq(Command, "status") || streq(Command, "limitmin")) {
	    snprintf(s, MAX_TEXT_LENGTH, "Team balance: (limitmin) Team limiting needs at least %d player(s).", PTB_LIMITMIN);
	    selfmessage(s);
	}
 
	// PTB_MAXSIZE
	if (streq(Command, "maxsize") && strlen(Argument)) {
	    PTB_MAXSIZE = strtonum(Argument);
	    if (PTB_MAXSIZE < 0) PTB_MAXSIZE = 0;
	}
	if (streq(Command, "status") || streq(Command, "maxsize")) {
	    snprintf(s, MAX_TEXT_LENGTH, "Team balance: (maxsize) Maximum team size is %d player(s).", PTB_MAXSIZE);
	    selfmessage(s);
	}
 
	// PTB_MAXDIFF
	if (streq(Command, "maxdiff") && strlen(Argument)) {
	    PTB_MAXDIFF = strtonum(Argument);
	    if (PTB_MAXDIFF < 1) PTB_MAXDIFF = 1;
	}
	if (streq(Command, "status") || streq(Command, "maxdiff")) {
	    snprintf(s, MAX_TEXT_LENGTH, "Team balance: (maxdiff) Maximum team size difference is %d.", PTB_MAXDIFF);
	    selfmessage(s);
	}
 
	// PTB_AUTOROUNDS
	if (streq(Command, "autorounds") && strlen(Argument)) {
	    PTB_AUTOROUNDS = strtonum(Argument);
	    if (PTB_AUTOROUNDS < 0) PTB_AUTOROUNDS = 0;
	}
	if (streq(Command, "status") || streq(Command, "autorounds")) {
	    snprintf(s, MAX_TEXT_LENGTH, "Team balance: (autorounds) First %d rounds no free team choice.", PTB_AUTOROUNDS);
	    selfmessage(s);
	}
 
	// PTB_WTJAUTO
	if (streq(Command, "wtjauto") && strlen(Argument)) {
	    PTB_WTJAUTO = strtonum(Argument);
	    if (PTB_WTJAUTO < 0) PTB_WTJAUTO = 0;
	}
	if (streq(Command, "status") || streq(Command, "wtjauto")) {
	    snprintf(s, MAX_TEXT_LENGTH, "Team balance: (wtjauto) Auto-joining WTJ after %d tr(y/ies).", PTB_WTJAUTO);
	    selfmessage(s);
	}
 
	// PTB_WTJKICK
	if (streq(Command, "wtjkick") && strlen(Argument)) {
	    PTB_WTJKICK = strtonum(Argument);
	    if (PTB_WTJKICK < 1) PTB_WTJKICK = 1;
	}
	if (streq(Command, "status") || streq(Command, "wtjkick")) {
	    snprintf(s, MAX_TEXT_LENGTH, "Team balance: (wtjauto) Auto-kicking WTJ after %d tr(y/ies).", PTB_WTJKICK);
	    selfmessage(s);
	}
 
	// PTB_KICK
	if (streq(Command, "kick") && strlen(Argument)) PTB_KICK = check_param(Argument);
	if (streq(Command, "status") || streq(Command, "kick")) { if (PTB_KICK) selfmessage("Team balance: (kick) WTJ kicking is ON."); else selfmessage("Team balance: (kick) Kicking is OFF."); }
 
	// PTB_SAVEWTJ
	if (streq(Command, "savewtj") && strlen(Argument)) PTB_SAVEWTJ = check_param(Argument);
	if (streq(Command, "status") || streq(Command, "savewtj")) { if (PTB_SAVEWTJ) selfmessage("Team balance: (savewtj) Saving to wtj.txt is ON."); else selfmessage("Team balance: (savewtj) Saving to wtj.txt is OFF."); }
 
	// team balancing actions
	if (streq(Command, "status")) selfmessage("Team balance: ---------- Team Balancing Actions ----------");
 
	// PTB_SWITCH
	if (streq(Command, "switch") && strlen(Argument)) PTB_SWITCH = check_param(Argument);
	if (streq(Command, "status") || streq(Command, "switch")) { if (PTB_SWITCH) selfmessage("Team balance: (switch) Team switching is ON."); else selfmessage("Team balance: (switch) Team switching is OFF."); }
 
	// PTB_SWITCHAFTER
	if (streq(Command, "switchafter") && strlen(Argument)) {
	    PTB_SWITCHAFTER = strtonum(Argument);
	    if (PTB_SWITCHAFTER < 0) PTB_SWITCHFREQ = 0;
	}
	if (streq(Command, "status") || streq(Command, "switchafter")) {
	    snprintf(s, MAX_TEXT_LENGTH, "Team balance: (switchafter) Switching starts after %d round(s).", PTB_SWITCHAFTER);
	    selfmessage(s);
	}
 
	// PTB_SWITCHMIN
	if (streq(Command, "switchmin") && strlen(Argument)) {
	    PTB_SWITCHMIN = strtonum(Argument);
	    if (PTB_SWITCHMIN < 0) PTB_SWITCHMIN = 0;
	}
	if (streq(Command, "status") || streq(Command, "switchmin")) {
	    snprintf(s, MAX_TEXT_LENGTH, "Team balance: (switchmin) Switching needs at least %d player(s).", PTB_SWITCHMIN);
	    selfmessage(s);
	}
 
	// PTB_PLAYERFREQ
	if (streq(Command, "playerfreq") && strlen(Argument)) {
	    PTB_PLAYERFREQ = strtonum(Argument);
	    if (PTB_PLAYERFREQ < 0) PTB_PLAYERFREQ = 0;
	}
	if (streq(Command, "status") || streq(Command, "playerfreq")) {
	    snprintf(s, MAX_TEXT_LENGTH, "Team balance: (playerfreq) Individual players are switched every %d round(s) at maximum.", PTB_PLAYERFREQ);
	    selfmessage(s);
	}
 
	// PTB_SWITCHFREQ
	if (streq(Command, "switchfreq") && strlen(Argument)) {
	    PTB_SWITCHFREQ = strtonum(Argument);
	    if (PTB_SWITCHFREQ < 1) PTB_SWITCHFREQ = 1;
	}
	if (streq(Command, "status") || streq(Command, "switchfreq")) {
	    snprintf(s, MAX_TEXT_LENGTH, "Team balance: (switchfreq) Switch occurs every %d round(s) at maximum.", PTB_SWITCHFREQ);
	    selfmessage(s);
	}
 
	// PTB_FORCESWITCH
	if (streq(Command, "forceswitch") && strlen(Argument)) {
	    PTB_FORCESWITCH = strtonum(Argument);
	    if (PTB_FORCESWITCH < 0) PTB_FORCESWITCH = 0;
	}
	if (streq(Command, "status") || streq(Command, "forceswitch")) {
	    snprintf(s, MAX_TEXT_LENGTH, "Team balance: (forceswitch) Forcing switch after %d unsuccessful switch(es).", PTB_FORCESWITCH);
	    selfmessage(s);
	}
 
	// PTB_DEADONLY
	if (streq(Command, "deadonly") && strlen(Argument)) PTB_DEADONLY = check_param(Argument);
	if (streq(Command, "status") || streq(Command, "deadonly")) { if (PTB_DEADONLY) selfmessage("Team balance: (deadonly) Switching dead only is ON."); else selfmessage("Team balance: (deadonly) Switching dead only is OFF."); }
 
	// messages
	if (streq(Command, "status")) selfmessage("Team balance: ---------- Messages ----------");
 
	// PTB_TELLWTJ
	if (streq(Command, "tellwtj") && strlen(Argument)) PTB_TELLWTJ = check_param(Argument);
	if (streq(Command, "status") || streq(Command, "tellwtj")) { if (PTB_TELLWTJ) selfmessage("Team balance: (tellwtj) Telling about WTJ tries is ON."); else selfmessage("Team balance: (tellwtj) Telling about WTJ tries is OFF."); }
 
	// PTB_ANNOUNCE
	if (streq(Command, "announce") && strlen(Argument)) PTB_ANNOUNCE = check_param(Argument);
	if (streq(Command, "status") || streq(Command, "announce")) { if (PTB_ANNOUNCE) selfmessage("Team balance: (announce) Announcements are ON."); else selfmessage("Team balance: (announce) Announcements are OFF."); }
 
	// PTB_SAYOK
	if (streq(Command, "sayok") && strlen(Argument)) PTB_SAYOK = check_param(Argument);
	if (streq(Command, "status") || streq(Command, "sayok")) { if (PTB_SAYOK) selfmessage("Team balance: (sayok) ^"OK^" announcements are ON."); else selfmessage("Team balance: (sayok) ^"OK^" announcements are OFF."); }
 
	// PTB_TYPESAY
	if (streq(Command, "typesay") && strlen(Argument)) PTB_TYPESAY = check_param(Argument);
	if (streq(Command, "status") || streq(Command, "typesay")) { if (PTB_TYPESAY) selfmessage("Team balance: (typesay) typesay usage is ON."); else selfmessage("Team balance: (typesay) typesay usage is OFF."); }
 
	// team strength limits
	if (streq(Command, "status")) selfmessage("Team balance: ---------- Team Strength Limits ----------");
 
	// PTB_MAXSTREAK
	if (streq(Command, "maxstreak") && strlen(Argument)) {
	    PTB_MAXSTREAK = strtonum(Argument);
	    if (PTB_MAXSTREAK < 1) PTB_MAXSTREAK = 1;
	}
	if (streq(Command, "status") || streq(Command, "maxstreak")) {
	    snprintf(s, MAX_TEXT_LENGTH, "Team balance: (maxstreak) Maximum accepted win streak is %d.", PTB_MAXSTREAK);
	    selfmessage(s);
	}
 
	// PTB_MAXSTREAK
	if (streq(Command, "maxscore") && strlen(Argument)) {
	    PTB_MAXSCORE = strtonum(Argument);
	    if (PTB_MAXSCORE < 1) PTB_MAXSCORE = 1;
	}
	if (streq(Command, "status") || streq(Command, "maxscore")) {
	    snprintf(s, MAX_TEXT_LENGTH, "Team balance: (maxscore) Maximum accepted team score difference is %d.", PTB_MAXSCORE);
	    selfmessage(s);
	}
 
	// PTB_MINRATING
	if (streq(Command, "minrating") && strlen(Argument)) {
    	    PTB_MINRATING = fixedstr(Argument);
    	    if (PTB_MINRATING < fixed(1)) PTB_MINRATING = fixed(1);
        }
	if (streq(Command, "status") || streq(Command, "minrating")) {
	    snprintf(s, MAX_TEXT_LENGTH, "Team balance: (minrating) Minimum critical strength rating is %d.%d.",
		fround(PTB_MINRATING, fround_floor),
		fround(fmul(fixed(10), ffract(PTB_MINRATING)), fround_floor)
	    );
	    selfmessage(s);
	}
 
	// PTB_MAXRATING
	if (streq(Command, "maxrating") && strlen(Argument)) {
	    PTB_MAXRATING = fixedstr(Argument);
	    if (PTB_MAXRATING < fixed(1)) PTB_MAXRATING = fixed(1);
	}
	if (streq(Command, "status") || streq(Command, "maxrating")) {
	    snprintf(s, MAX_TEXT_LENGTH, "Team balance: (maxrating) Maximum critical strength rating is %d.%d.",
		fround(PTB_MAXRATING, fround_floor),
		fround(fmul(fixed(10), ffract(PTB_MAXRATING)), fround_floor)
	    );
	    selfmessage(s);
	}
 
	// PTB_SUPERRATING
	if (streq(Command, "superrating") && strlen(Argument)) {
	    PTB_SUPERRATING = fixedstr(Argument);
	    if (PTB_SUPERRATING < fixed(1)) PTB_SUPERRATING = fixed(1);
	}
	if (streq(Command, "status") || streq(Command, "superrating")) {
	    snprintf(s, MAX_TEXT_LENGTH, "Team balance: (superrating) Super critical strength rating is %d.%d.",
		fround(PTB_SUPERRATING, fround_floor),
		fround(fmul(fixed(10), ffract(PTB_SUPERRATING)), fround_floor)
	    );
	    selfmessage(s);
	}
 
	// misc
	if (streq(Command, "status")) {
	    selfmessage("Team balance: ---------- Misc ----------");
	    selfmessage("Team balance: To enable or disable PTB, type ^"admin_teambalance <^"on^"|^"1^"|^"off^"|^"0^">^".");
	    selfmessage("Team balance: To view or change a single PTB setting, type ^"admin_teambalance <setting> <^"on^"|^"off^"|value>^".");
	    selfmessage("Team balance: To view a brief overview of PTB commands, type ^"admin_teambalance help^" or ^"admin_teambalance list^".");
	    selfmessage("Team balance: To save all PTB settings to ^"vault.ini^", type ^"admin_teambalance save^".");
	    selfmessage("Team balance: To load all PTB settings from ^"vault.ini^", type ^"admin_teambalance load^".");
	    selfmessage("Team balance: For PTB statistics, simply type ^"admin_teambalance^".");
	}
 
	return PLUGIN_HANDLED;
}
 
 
stock saveSettings () {
	new VAULTDATA[MAX_DATA_LENGTH];
 
	// team selection control
	if (PTB_LIMITJOIN) set_vaultdata("PTB_LIMITJOIN", "on");
	else set_vaultdata("PTB_LIMITJOIN", "off");
	snprintf(VAULTDATA, MAX_DATA_LENGTH, "%d", PTB_LIMITAFTER);
	set_vaultdata("PTB_LIMITAFTER", VAULTDATA);
	snprintf(VAULTDATA, MAX_DATA_LENGTH, "%d", PTB_LIMITMIN);
	set_vaultdata("PTB_LIMITMIN", VAULTDATA);
	snprintf(VAULTDATA, MAX_DATA_LENGTH, "%d", PTB_MAXSIZE);
	set_vaultdata("PTB_MAXSIZE", VAULTDATA);
	snprintf(VAULTDATA, MAX_DATA_LENGTH, "%d", PTB_MAXDIFF);
	set_vaultdata("PTB_MAXDIFF", VAULTDATA);
	snprintf(VAULTDATA, MAX_DATA_LENGTH, "%d", PTB_AUTOROUNDS);
	set_vaultdata("PTB_AUTOROUNDS", VAULTDATA);
	snprintf(VAULTDATA, MAX_DATA_LENGTH, "%d", PTB_WTJAUTO);
	set_vaultdata("PTB_WTJAUTO", VAULTDATA);
	snprintf(VAULTDATA, MAX_DATA_LENGTH, "%d", PTB_WTJKICK);
	set_vaultdata("PTB_WTJKICK", VAULTDATA);
	snprintf(VAULTDATA, MAX_DATA_LENGTH, "%d", PTB_KICK);
	set_vaultdata("PTB_KICK", VAULTDATA);
	if (PTB_SAVEWTJ) set_vaultdata("PTB_SAVEWTJ", "on");
	else set_vaultdata("PTB_SAVEWTJ", "off");
 
	// team balancing actions
	if (PTB_SWITCH) set_vaultdata("PTB_SWITCH", "on");
	else set_vaultdata("PTB_SWITCH", "off");
	snprintf(VAULTDATA, MAX_DATA_LENGTH, "%d", PTB_SWITCHAFTER);
	set_vaultdata("PTB_SWITCHAFTER", VAULTDATA);
	snprintf(VAULTDATA, MAX_DATA_LENGTH, "%d", PTB_SWITCHMIN);
	set_vaultdata("PTB_SWITCHMIN", VAULTDATA);
	snprintf(VAULTDATA, MAX_DATA_LENGTH, "%d", PTB_SWITCHFREQ);
	set_vaultdata("PTB_SWITCHFREQ", VAULTDATA);
	snprintf(VAULTDATA, MAX_DATA_LENGTH, "%d", PTB_PLAYERFREQ);
	set_vaultdata("PTB_PLAYERFREQ", VAULTDATA);
	snprintf(VAULTDATA, MAX_DATA_LENGTH, "%d", PTB_FORCESWITCH);
	set_vaultdata("PTB_FORCESWITCH", VAULTDATA);
	if (PTB_DEADONLY) set_vaultdata("PTB_DEADONLY", "on");
	else set_vaultdata("PTB_DEADONLY", "off");
 
	// messages
	if (PTB_TELLWTJ) set_vaultdata("PTB_TELLWTJ", "on");
	else set_vaultdata("PTB_TELLWTJ", "off");
	if (PTB_ANNOUNCE) set_vaultdata("PTB_ANNOUNCE", "on");
	else set_vaultdata("PTB_ANNOUNCE", "off");
	if (PTB_SAYOK) set_vaultdata("PTB_SAYOK", "on");
	else set_vaultdata("PTB_SAYOK", "off");
	if (PTB_TYPESAY) set_vaultdata("PTB_TYPESAY", "on");
	else set_vaultdata("PTB_TYPESAY", "off");
 
	// team strength limits
	snprintf(VAULTDATA, MAX_DATA_LENGTH, "%d", PTB_MAXSTREAK);
	set_vaultdata("PTB_MAXSTREAK", VAULTDATA);
	snprintf(VAULTDATA, MAX_DATA_LENGTH, "%d", PTB_MAXSCORE);
	set_vaultdata("PTB_MAXSCORE", VAULTDATA);
	snprintf(VAULTDATA, MAX_DATA_LENGTH, "%d.%d",
	    fround(PTB_MINRATING, fround_floor),
	    fround(fmul(fixed(10), ffract(PTB_MINRATING)), fround_floor)
	);
	set_vaultdata("PTB_MINRATING", VAULTDATA);
	snprintf(VAULTDATA, MAX_DATA_LENGTH, "%d.%d",
	    fround(PTB_MAXRATING, fround_floor),
	    fround(fmul(fixed(10), ffract(PTB_MAXRATING)), fround_floor)
	);
	set_vaultdata("PTB_MAXRATING", VAULTDATA);
	snprintf(VAULTDATA, MAX_DATA_LENGTH, "%d.%d",
	    fround(PTB_SUPERRATING, fround_floor),
	    fround(fmul(fixed(10), ffract(PTB_SUPERRATING)), fround_floor)
	);
	set_vaultdata("PTB_SUPERRATING", VAULTDATA);
}
 
 
stock loadSettings () {
	new VAULTDATA[MAX_DATA_LENGTH];
 
	// team selection control
	if (get_vaultdata("PTB_LIMITJOIN", VAULTDATA, MAX_DATA_LENGTH ) != 0)
		PTB_LIMITJOIN = check_param(VAULTDATA);
	if (get_vaultdata("PTB_LIMITAFTER", VAULTDATA, MAX_DATA_LENGTH ) != 0)
		PTB_LIMITAFTER = strtonum(VAULTDATA);
	if (get_vaultdata("PTB_LIMITMIN", VAULTDATA, MAX_DATA_LENGTH ) != 0)
		PTB_LIMITMIN = strtonum(VAULTDATA);
	if (get_vaultdata("PTB_MAXSIZE", VAULTDATA, MAX_DATA_LENGTH ) != 0)
		PTB_MAXSIZE = strtonum(VAULTDATA);
	if (get_vaultdata("PTB_MAXDIFF", VAULTDATA, MAX_DATA_LENGTH ) != 0)
		PTB_MAXDIFF = strtonum(VAULTDATA);
	if (get_vaultdata("PTB_AUTOROUNDS", VAULTDATA, MAX_DATA_LENGTH ) != 0)
		PTB_AUTOROUNDS = strtonum(VAULTDATA);
	if (get_vaultdata("PTB_WTJAUTO", VAULTDATA, MAX_DATA_LENGTH ) != 0)
		PTB_WTJAUTO = strtonum(VAULTDATA);
	if (get_vaultdata("PTB_WTJKICK", VAULTDATA, MAX_DATA_LENGTH ) != 0)
		PTB_WTJKICK = strtonum(VAULTDATA);
	if (get_vaultdata("PTB_KICK", VAULTDATA, MAX_DATA_LENGTH ) != 0)
		PTB_KICK = check_param(VAULTDATA);
	if (get_vaultdata("PTB_SAVEWTJ", VAULTDATA, MAX_DATA_LENGTH ) != 0)
		PTB_SAVEWTJ = check_param(VAULTDATA);
 
	// team balancing actions
	if (get_vaultdata("PTB_SWITCH", VAULTDATA, MAX_DATA_LENGTH ) != 0)
		PTB_SWITCH = check_param(VAULTDATA);
	if (get_vaultdata("PTB_SWITCHAFTER", VAULTDATA, MAX_DATA_LENGTH ) != 0)
		PTB_SWITCHAFTER = strtonum(VAULTDATA);
	if (get_vaultdata("PTB_SWITCHMIN", VAULTDATA, MAX_DATA_LENGTH ) != 0)
		PTB_SWITCHMIN = strtonum(VAULTDATA);
	if (get_vaultdata("PTB_SWITCHFREQ", VAULTDATA, MAX_DATA_LENGTH ) != 0)
		PTB_SWITCHFREQ = strtonum(VAULTDATA);
	if (get_vaultdata("PTB_PLAYERFREQ", VAULTDATA, MAX_DATA_LENGTH ) != 0)
		PTB_PLAYERFREQ = strtonum(VAULTDATA);
	if (get_vaultdata("PTB_FORCESWITCH", VAULTDATA, MAX_DATA_LENGTH ) != 0)
		PTB_FORCESWITCH = strtonum(VAULTDATA);
	if (get_vaultdata("PTB_DEADONLY", VAULTDATA, MAX_DATA_LENGTH ) != 0)
		PTB_DEADONLY = check_param(VAULTDATA);
 
	// messages
	if (get_vaultdata("PTB_TELLWTJ", VAULTDATA, MAX_DATA_LENGTH ) != 0)
		PTB_TELLWTJ = check_param(VAULTDATA);
	if (get_vaultdata("PTB_ANNOUNCE", VAULTDATA, MAX_DATA_LENGTH ) != 0)
		PTB_ANNOUNCE = check_param(VAULTDATA);
	if (get_vaultdata("PTB_SAYOK", VAULTDATA, MAX_DATA_LENGTH ) != 0)
		PTB_SAYOK = check_param(VAULTDATA);
	if (get_vaultdata("PTB_TYPESAY", VAULTDATA, MAX_DATA_LENGTH ) != 0)
		PTB_TYPESAY = check_param(VAULTDATA);
 
	// team strength limits
	if (get_vaultdata("PTB_MAXSTREAK", VAULTDATA, MAX_DATA_LENGTH ) != 0)
		PTB_MAXSTREAK = strtonum(VAULTDATA);
	if (get_vaultdata("PTB_MAXSCORE", VAULTDATA, MAX_DATA_LENGTH ) != 0)
		PTB_MAXSCORE = strtonum(VAULTDATA);
	if (get_vaultdata("PTB_MINRATING", VAULTDATA, MAX_DATA_LENGTH ) != 0)
		PTB_MINRATING = fixedstr(VAULTDATA);
	if (get_vaultdata("PTB_MAXRATING", VAULTDATA, MAX_DATA_LENGTH ) != 0)
		PTB_MAXRATING = fixedstr(VAULTDATA);
	if (get_vaultdata("PTB_SUPERRATING", VAULTDATA, MAX_DATA_LENGTH ) != 0)
		PTB_SUPERRATING = fixedstr(VAULTDATA);
}
 
 
stock displayStatistics (toLog = false) {
	//say("displayStatistics");
 
	new text[MAX_TEXT_LENGTH];
 
	// time
	snprintf(text, MAX_TEXT_LENGTH, "Team Balance: Statistics generated at: %s", lastTeamBalanceCheck);
	if (toLog) {
            log(text);
	} else {
            selfmessage(text);
        }
 
	// connected players
	snprintf(text, MAX_TEXT_LENGTH, "Team Balance: Connected players: %d", playercount());
	if (toLog) {
            log(text);
	} else {
            selfmessage(text);
        }
 
	// team sizes
	snprintf(text, MAX_TEXT_LENGTH, "Team Balance: Team sizes: CTs %d, Ts %d", teamCounts[CTS], teamCounts[TS]);
	if (toLog) {
            log(text);
	} else {
            selfmessage(text);
        }
 
	// team scores
	snprintf(text, MAX_TEXT_LENGTH, "Team Balance: Team scores: CTs %d, Ts %d", teamScores[CTS], teamScores[TS]);
	if (toLog) {
            log(text);
	} else {
            selfmessage(text);
        }
 
	// Kills:Deaths
	snprintf(text, MAX_TEXT_LENGTH, "Team Balance: Team kills:deaths: CTs %d:%d, Ts %d:%d", teamKills[CTS], teamDeaths[CTS], teamKills[TS], teamDeaths[TS]);
	if (toLog) {
            log(text);
	} else {
            selfmessage(text);
        }
 
	// Kills/Deaths
	snprintf(text, MAX_TEXT_LENGTH, "Team Balance: Team kills/deaths: CTs %d.%d, Ts %d.%d",
	            fround(ctKD, fround_floor),
	            fround(fmul(fixed(100), ffract(ctKD)), fround_floor),
	            fround(tKD, fround_floor),
	            fround(fmul(fixed(100), ffract(tKD)), fround_floor)
	);
	if (toLog) {
            log(text);
	} else {
            selfmessage(text);
        }
 
	// strength
	snprintf(text, MAX_TEXT_LENGTH, "Team Balance: Team strengths: CTs %d.%d, Ts %d.%d",
	            fround(ctStrength, fround_floor),
	            fround(fmul(fixed(100), ffract(ctStrength)),
	            fround_floor), fround(tStrength, fround_floor),
	            fround(fmul(fixed(100), ffract(tStrength)), fround_floor)
	);
	if (toLog) {
            log(text);
	} else {
            selfmessage(text);
        }
 
	// rating
	snprintf(text, MAX_TEXT_LENGTH, "Team Balance: Team ratings: CTs %d.%d, Ts %d.%d",
	            fround(ctRating, fround_floor),
	            fround(fmul(fixed(100), ffract(ctRating)), fround_floor),
	            fround(tRating, fround_floor),
	            fround(fmul(fixed(100), ffract(tRating)), fround_floor)
	);
	if (toLog) {
            log(text);
	} else {
            selfmessage(text);
        }
 
	// won rounds
	if (winStreaks[CTS] > 0) {
	    snprintf(text, MAX_TEXT_LENGTH, "Team Balance: Last %d round(s) won by CTs.", winStreaks[CTS]);
	    if (toLog) {
                log(text);
	    }else {
                selfmessage(text);
            }
	} else if (winStreaks[TS] > 0) {
	    snprintf(text, MAX_TEXT_LENGTH, "Team Balance: Last %d round(s) won by Ts.", winStreaks[TS]);
	    if (toLog) {
                log(text);
	    }else {
                selfmessage(text);
            }
	}
 
	// winning team
	if (winnerTeam == CTS) {
            snprintf(text, MAX_TEXT_LENGTH, "Team Balance: The CTs are the winning team.");
	} else if (winnerTeam == TS) {
            snprintf(text, MAX_TEXT_LENGTH, "Team Balance: The Ts are the winning team.");
	} else {
            snprintf(text, MAX_TEXT_LENGTH, "Team Balance: Teams are balanced.");
        }
	if (toLog) {
            log(text);
	} else {
            selfmessage(text);
        }
 
	snprintf(text, MAX_TEXT_LENGTH, "Team Balance: (These statistics might be already outdated.)");
	if (toLog) {
            log(text);
	} else {
            selfmessage(text);
        }
 
	snprintf(text, MAX_TEXT_LENGTH, "Team Balance: To view a brief overview of Team balance commands, type ^"admin_teambalance help^" or ^"admin_teambalance list^".");
	if (toLog) {
            log(text);
	} else {
            selfmessage(text);
        }
 
	snprintf(text, MAX_TEXT_LENGTH, "Team Balance: To view all Team balance settings, type ^"admin_teambalance status^".");
	if (toLog) {
            log(text);
	} else {
            selfmessage(text);
        }
}
 
 
public plugin_connect (HLUserName, HLIP, UserIndex)
{
	//say("plugin_connect");
 
	if ((UserIndex >= 1) && (UserIndex < MAX_PLAYERS)) {
	    kills[UserIndex] = 0;
	    deaths[UserIndex] = 0;
	    isChoosingTeam[UserIndex] = 1;
	    isBeingTransfered[UserIndex] = 0;
	    playerTeam[UserIndex] = 0;
	    lastRoundSwitched[UserIndex] = 0;
	    wtjCount[UserIndex] = 0;
	} else {
	    say("Team Balance: Shouldn't get here, unexpected UserIndex.");
	}
	return PLUGIN_CONTINUE;
}
 
 
public plugin_disconnect (HLUserName, UserIndex)
{
	//say("plugin_disconnect");
 
	//new debug[MAX_DATA_LENGTH];
	//snprintf(debug, MAX_DATA_LENGTH, "[DEBUG] Team balance plugin_disconnect: %d %d", UserIndex, playerTeam[UserIndex]);
	//log(debug);
 
	if ((UserIndex >= 1) && (UserIndex < MAX_PLAYERS)) {
	    kills[UserIndex] = 0;
	    deaths[UserIndex] = 0;
	    isChoosingTeam[UserIndex] = 0;
	    isBeingTransfered[UserIndex] = 0;
	    if (playerTeam[UserIndex] == TS || playerTeam[UserIndex] == CTS) {
	        teamCounts[playerTeam[UserIndex]] -= 1;
	    }
	    playerTeam[UserIndex] = 0;
	    lastRoundSwitched[UserIndex] = 0;
	    wtjCount[UserIndex] = 0;
	} else {
	    say("Team Balance: Shouldn't get here, unexpected UserIndex.");
	}
 
	// redundant team size check
	teamCounts[CTS] = 0;
	teamCounts[TS] = 0;
	teamCounts[0] = 0;
	for (new i=1; i<MAX_PLAYERS; ++i) teamCounts[playerTeam[i]] += 1;
 
	//displayStatistics(true);
 
	return PLUGIN_CONTINUE;
}
 
 
public plugin_init ()
{
	//initVariables();
 
	// must do this here
	PTB_MINRATING = fixedstr(MIN_RATING);
	PTB_MAXRATING = fixedstr(MAX_RATING);
	PTB_SUPERRATING = fixedstr(SUPER_RATING);
 
	plugin_registerinfo("Team balance plugin", "Evens the teams by moving/switching players each round.", STRING_VERSION);
	plugin_registercmd("admin_teambalance", "admin_teambalance", ACCESS_ALL, "admin_teambalance <^"on^" | ^"off^" | ^"status^" | ^"^">: Switch Team balance on or off, get status or statistics.");
 
	plugin_registercmd("chooseteam", "ChoosingTeam", ACCESS_ALL);
	plugin_registercmd("menuselect", "MenuSelect", ACCESS_ALL);
	plugin_registercmd("radio", "NotChoosingTeam", ACCESS_ALL);
	plugin_registercmd("buy", "NotChoosingTeam", ACCESS_ALL);
	plugin_registercmd("buyequip", "NotChoosingTeam", ACCESS_ALL);
	plugin_registercmd("showbriefing", "NotChoosingTeam", ACCESS_ALL);
 
	plugin_registercmd("ptb_teamselection", "ptb_teamselection", ACCESS_CONSOLE);
	plugin_registercmd("ptb_kill", "ptb_kill", ACCESS_CONSOLE);
	plugin_registercmd("ptb_teamaction", "ptb_teamaction", ACCESS_CONSOLE);
	plugin_registercmd("ptb_worldaction", "ptb_worldaction", ACCESS_CONSOLE);
	plugin_registercmd("ptb_teamscore", "ptb_teamscore", ACCESS_CONSOLE);
 
	exec("sm_register TeamInfo ^"admin_command ptb_teamselection^" ad");
	exec("sm_register DeathMsg ^"admin_command ptb_kill^" ad");
	exec("sm_register SendAudio ^"admin_command ptb_teamaction^" ad ^"2=%!MRAD_terwin^" ^"2=%!MRAD_ctwin^" ^"2=%!MRAD_rounddraw^"");
	exec("sm_register SM_RoundStart ^"admin_command ptb_worldaction^" bdc");
	exec("sm_register TeamScore ^"admin_command ptb_teamscore^" ad");
 
	loadSettings();
 
    	PTB_MAXSIZE = maxplayercount()/2;
 
	return PLUGIN_CONTINUE;
}