/*
 
	Ptahhotep's Team Balancer (PTB)
	Version 1.7 BETA
 
	DATE: June 28, 2002
	FILE: plugin_logd_cs_ptb.sma
	AUTHOR: Ptahhotep (ptahhotep@planethalflife.com)
	CONTRIBUTIONS: 
	o NtroP started some new team switching code for PTB 1.5,
	  choosing the switch/transfer which balances team strengths best.
	TESTERS: all the great guys at http://www.concarne.org
 
	Thanks to all those who reported bugs and sent in suggestions!
	Special thanks go to Innocence from http://www.seth.dk, who maintains
	a small PTB FAQ.
 
	Comments On Version 1.7
	-----------------------
	o This version adds another feature to PTB, which scales down the
	  the internally counted player scores. If the sum of kills and
	  deaths for a player exceed PTB_MAXINCIDENTS, the scores are
	  divided by PTB_SCALEDOWN. This insures, that consecutive kills
	  or deaths of a player have a higher influence on his kills/deaths
	  ratio. A single kill of a player with a 500/300 score would mean
	  nothing. The new system insures, that this score would be
	  represented by an equivalent 25/15 or similar and every consecutive
	  kill or death would influence the ratio noticibly. That way the
	  rating of a player is representing the player's current performance
	  way better in long games (read: dust-only over 7 hours ...).
	o There reported some problems with servers running for 18 hours
	  or more. PTB started reporting stack overflows (amx error 3) at
	  each kill. This version should resolve these problems, since the
	  source of the error (AdminMod's native function playerinfo) is
	  now either not used anymore or feeded very carefully. If that
	  doesn't help, it's possible to get rid of the stack overflow
	  problem with an increase of the stack frame (change the according
	  line in the script to #pragma dynamic 262144 and recompile).
 
	Comments On Version 1.6
	-----------------------
	o This version adds another 8 new configuration options to PTB.
	o Setting the new max team size setting to a number smaller than half
	  the number of maximum players on the server effectively forces the
	  remaining players into spectator mode.
	o I put some real work into the admin_ptb command. Please check out
	  the "admin_ptb" and "admin_ptb status" commands from the CS console.
	o PTB now limits the frequency of switches of individual players,
	  which should help getting rid of complaints about frequent switching.
	o PTB 1.6 uses the AdminMod playercount() function to determine
	  the current number of players in the game. The number returned by
	  that function is not always correct. Please keep that in mind when
	  using the new PTB_SWITCHMIN and PTB_LIMITMIN settings. You can
	  monitor the value returned by playerinfo() by typing admin_ptb in
	  the console. I also tried to track the number of connections and
	  disconnections, but that gives even worse results, since AdminMod
	  doesn't seem to generate either enough disconnect or more than
	  enough connect events.
	o The new commands "admin_ptb save" and "admin_ptb load" let you
	  save and restore changes in the settings made from the console.
	  That way you can experiment with the PTB settings in-game and
	  make them permanent without editing any files by hand.
	o The new PTB_SUPERRATING setting adds a new winning team condition,
	  that effectively limits the maximum allowed team strength for the
	  losing team. Before version 1.6 PTB allowed a team to be as strong
	  as it gets, as long as it didn't have either a winning streak or
	  a score advantage or both. See the Details section for details.
	o The new commands "admin_ptb list" and "admin_ptb help" both give
	  you a brief overview of available PTB commands.
 
	Comments On Version 1.5
	-----------------------
	This verion of PTB is tested with AdminMod 2.50.26a, Metamod 1.12
	and LogD 1.00.3 (Wraith's version ...).
	This version improves PTB in a lot of ways:
	o Most important is the new team switching code, which looks
	  for the best possible switch to execute. If there is no switch
	  that makes any difference in team balance, no switching happens.
	  Thanks go to NtroP for pushing me to finish his team
	  switching code for version 1.5 of PTB.
	o Also new is an improved team strength calculation method,
	  providing better indication of team strength. A detailed
	  description is given in the Details section.
	o Players will not be allowed to choose their team in the first
	  3 rounds, instead they will be auto-joined.
	o PTB will now try to keep the differences in team size below 3
	  when auto-joining players.
	o Forcing a switch (switching alive) is now configurable. You
	  can set the number of unsuccessful tries, after which PTB
	  does a switch of living players (killing them in the process).
	o Default settings for wtj auto-joining and kicking are much
	  more strict now. Try wtjing a second time and PTB will auto-
	  join you, try it a third time and you get kicked.
	o There is now an option to disable typesays completely.
	o All the things above are configurable through "vault.ini" -
	  another new feature of PTB 1.5.
	o Finally some bugs were fixed and some additional changes might
	  have happened that I forgot to note.
	See the latest changes notes below and the new section about
	vault.ini for more details.
 
	Introduction
	------------
	PTB is an Adminmod plugin (http://www.adminmod.org).
	LogD (http://logd.sourceforge.net) is required to run PTB.
	PTB is a Counter-Strike (CS) specific plugin.
 
	To run PTB with its default settings, simply compile it
	for your operating system, include it into plugin.ini
	and change the map on your CS server.
 
	PTB detects situations, in which one team rules over the other.
	It looks at the current team scores, current team win streaks
	and the current team ratings and decides on actions with the
	help of these values.
 
	Additionally PTB completely controls team joining actions of
	players and prevents unbalancing team joins. PTB auto-joins
	and even kicks obvious winning team joiners.
 
	Features
	--------
	o team strength evaluation based on scores, streaks, kills, deaths and size
	o search for best balancing switch or transfer
	o automatic dead only team switches
	o automatic dead only team transfers
	o announcement of team balance status
	o display of WTJ attempts
	o prevention, auto-join and kick for WTJing
	o "safe auto-join" - overriden auto-join function, that is always
	  choosing the right team and prevents joining of the same or
	  winning or bigger team, using auto-team never kills a player,
	  if he can't switch teams, and never gives WTJ counts
	o controllable with 33 console commands
	o configurable with 26 configuration options
	o configurable through vault.ini
	o much more ...
 
	Details
	-------
	The most important thing PTB evaluates is team strength. PTB calculates
	a single number as an indicator of team strength.
 
	In PTB 1.5 the team strength calculation changed. In earlier versions,
	team strength was calculated as the sum of total kills per team over the
	sum of total deaths per team times the number of players per team
	(strength = kills/deaths * players). The method didn't always work well,
	since a very good player (20:1) and a very bad player (1:20) made no
	difference to 2 mediocre players (5:5 and 6:6).
 
	The new method calculates a separate kills/deaths ratio per player, sums
	the ratios up per team and multiplies them with team size
	(strength = players * (sum of kills/deaths per player)).
 
	 Example:
	 CT team: A (6:5), B (2:3)
	 T team: C (1:1), D (4:3), E (1:4)
	 CT strength: 2 * (6/5 + 2/3) = 3.73
	 T strength: 3 * (1/1 + 4/3 + 1/4) = 7.75
 
	To compare the team strengths against each other, a rating is calculated
	per team by dividing the team's	strength by the opposing team's strength
	(rating = ownStrength / opposingStrength).
 
	 Example (continued):
	 CT rating: 3.73/7.75 = 0.48
	 T rating: 7.75/3.73 = 2.08
 
	Just in case you want to know: the old ratings would have been 0.88 for
	the CT team and 1.125 for the T team. Just imagine to add player F (0:10)
	to the CT team and look what happens to the old and new ratings. I found
	the new ratings to represent the teams much better.
 
	But team rating is not all. Even the best team might fail. That's why PTB
	doesn't limit itself to those kill/death ratios. PTB also compares the number
	of team wins and notes the number of consecutive rounds one by a team as
	additional indicators for team efficiency.
 
	To decide, if there if there is a winning team situation, PTB then evaluates
	the following equations:
 
	o team rating >= PTB_MINRATING (stronger team) and score > PTB_MAXDIFF
	o team rating >= PTB_MINRATING (stronger team) and streak > PTB_MAXSTREAK
	o team rating >= PTB_MINRATING (stronger team) and more players
	o team rating >= PTB_MAXRATING (way stronger team)
	o team rating >= PTB_SUPERRATING (overwhelming team strength)
 
	If any TWO of these criteria hold for one team, it is considered to
	be the winning team and actions are taken as appropriate.
 
	Note: Teams with a rating below PTB_MINRATING are considered as of equal
	or very similar strength. If team strengths are very similar, streaks
	and	scores are ignored. As soon as a team gets noticibly stronger
	than it's opposing team, scores, streaks and team size are checked.
 
	PTB balances the teams in one of two ways. If the losing team is smaller,
	PTB transfers a player from the winning to the losing team. Else two players
	are exchanged (switched) with each other. Valid targets for switches and
	transfers are dead players and players, who didn't change teams already
	this round. These limitations origin from the fact, that it's not possible
	to switch a living player with the help of console commands without killing
	him. Additionally CS currently does allow only one team change per round.
	Since PTB 1.5 all possible transfers or switches are evaluated for the
	switch or tranfer, that balances the teams the most. If no switch or transfer
	would improve team balance, switching or transfering is skipped.
 
	Configuration
	-------------
	PTB Required Variable Settings:
	allow_client_exec 1 - needed to switch players' teams
	admin_balance_teams 0 - switch other team balancing methods off
	mp_autoteambalance 0 - switch off inbuild CS team balancing
	mp_limitteams 10 - enable any team size difference
 
	PTB Commands:
 
	// misc
	admin_ptb - show PTB statistics (ACCESS_ALL)
	admin_ptb status - show all current settings (ACCESS_ALL)
	admin_ptb list - list of ptb commands (ACCESS_ALL)
	admin_ptb help - same as "list" (ACCESS_ALL)
	admin_ptb on - enable all options
	admin_ptb off - disable all options
	admin_ptb save - save all settings to vault.ini
	admin_ptb load - load all settings from vault.ini
 
	// team selection control
	admin_ptb limitjoin on|off (default: on) - team join control
	admin_ptb limitafter <value> (default: 0) - rounds after which teams limiting begins
	admin_ptb limitmin <value> (default: 0) - minimum players for team limiting
	admin_ptb maxsize <value> (default: 10) - maximum team size per team
	admin_ptb autorounds <value> (default: 3) - initial rounds without free team choice
	admin_ptb maxdiff <value> (default: 2) - maximum accepted team size difference
	admin_ptb wtjauto <value> (default: 2) - WTJ tries for auto-join
	admin_ptb wtjkick <value> (default: 3) - WTJ tries for kick
	admin_ptb kick on|off (default: on) - WTJ kicking
	admin_ptb savewtj on|off (default: off) - wtj.log writing
 
	// team balancing actions
	admin_ptb switch on|off (default: on) - team switching and transfers
	admin_ptb switchafter <value> (default: 0) - rounds after which switching begins
	admin_ptb switchmin <value> (default: 3) - minimum players on map for switching
	admin_ptb switchfreq <value> (default: 1) - maximum team switching frequency
	admin_ptb playerfreq <value> (default: 3) - maximum player switching frequency
	admin_ptb forceswitch <value> (default: 3) - forced switch delay (switching alive, if neccessary)
	admin_ptb deadonly on|off (default: on) - switch dead only or alive, too
 
	// messages
	admin_ptb tellwtj on|off (default: on) - tell about wtj tries
	admin_ptb announce on|off (default: on) - announcements
	admin_ptb sayok on|off (default: on) - "no action required" message
	admin_ptb typesay on|off (default: on) - globally switch use of typesay
 
	// team strength limits
	admin_ptb maxstreak <value> (default: 2) - maximum accepted team win streak
	admin_ptb maxscore <value> (default: 2) - maximum accepted team score difference
	admin_ptb minrating <value> (default: 1.5) - stronger team, if rating >= minrating
	admin_ptb maxrating <value> (default: 2.0) - way stronger team, if rating >= maxrating
	admin_ptb superrating <value> (default: 3.0) - overwhelming team strength, if rating >= superrating
	admin_ptb maxincidents <value> (default: 50) - maximum incidents per player before scale down
	admin_ptb scaledown <value> (default: 2) - scale down factor for player incidents
	 (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_ptb 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
	PTB_MAXINCIDENTS 50
	PTB_SCALEDOWN    2
 
	Latest Changes
	--------------
	Version 1.7
	+ NEW: added player rating scale down (PTB_MAXINCIDENTS and PTB_SCALEDOWN)
	+ FIXED: calls to playerinfo now feeded carefully
 
	Latest Changes
	--------------
	Version 1.6
	+ NEW: added new winning team condition (PTB_SUPERRATING)
	+ NEW: new console commands "admin_ptb list" and "admin_ptb help"
	+ NEW: new console command "admin_ptb save" saves settings to "vault.ini"
	+ NEW: new console command "admin_ptb load" loads settings from "vault.ini"
	+ NEW: grouped settings and added missing console commands
	+ NEW: rounds before team limiting setting (PTB_LIMITAFTER, default: 0)
	+ NEW: minimum players for team limiting setting (PTB_LIMITMIN, default: 0)
	+ NEW: maximum team size setting (PTB_MAXSIZE, default: 10)
	+ NEW: rounds before switching setting (PTB_SWITCHAFTER, default: 0)
	+ NEW: minimum players for switching setting (PTB_SWITCHMIN, default: 3)
	+ NEW: maximum switch frequency setting (PTB_SWITCHFREQ, default: 1)
	+ NEW: tell about WTJ tries setting (PTB_TELLWTJ, default: on)
	+ NEW: maximum player switch frequency setting (PTB_PLAYERFREQ, default: 3)
	+ CHANGED: replaced all option_ identifiers with their PTB_ counterparts
	+ CHANGED: further code cleaning
	+ FIXED: hopefully improved the "negative result on fdiv" workaround
 
	Version 1.5
	+ NEW: new team rating calculation method with much improved strength indication
	+ NEW: much improved team switching code, choosing best balancing switch/transfer
	  (thanks to NtroP for providing part of the code for this)
	+ NEW: reading configuration options from vault.ini (see section about vault.ini)
	+ NEW: option to globally disable typesay messages (PTB_TYPESAY, default: 1)
	+ NEW: wtj tries for autojoin setting (PTB_WTJAUTO, default: 2)
	+ NEW: wtj tries for kick setting (PTB_WTJKICK, default: 3)
	+ NEW: max team size difference setting (PTB_MAXDIFF, default: 2)
	+ NEW: auto join only in the first x rounds setting (PTB_AUTOROUNDS, default: 3)
	+ NEW: force switch after x unsuccessful tries setting (PTB_FORCESWITCH, default: 3)
	+ CHANGED: changed file name to plugin_logd_cs_ptb.sma
	+ CHANGED: added new documentation topics and updated the old ones
	+ REMOVED: removed option_maxrandom, as there is no random switching anymore
	+ FIXED: didn't catch all win conditions (e. g. Bomb_Defused was not counted)
 
	Version 1.4a (never published intermediate version)
	+ FIXED: Some semicolons missed, causing problems with AdminMod 2.50.26 (Metamod 1.12 version)
 
	Version 1.4
	+ FIXED: PTB didn't work the first time a player chooses a team
	+ NEW: PTB sets team strength to team size, if team has no single kill yet
	  (fix for team inbalance in the first rounds of a map)
	+ NEW: switching/transfering alive, if dead only is not possible repeatedly
 
	Version 1.3
	+ NEW: announcing team switch/transfer failures as typesay message
	+ NEW: added "sayok" option
	+ FIXED: completely reworked the options interface, which didn't work at all
	+ WORKAROUND: retrying calculation for negative results on division of 2 positive numbers
	+ REMOVED: command admin_ptb_stats no longer exists
 
	Version 1.2a
	+ FIXED: compile error with Adminmod 2.50.26 (plugin_connect syntax change in Adminmod?)
 
	Version 1.2
	+ NEW: display WTJ count
	+ NEW: kick for WTJ count 10 or greater
	+ NEW: force players into losing/smaller team for WTJ count > 3
	+ NEW: save player name and WONID in wtj.log for WTJ count > 3
	+ NEW: changed rating to incorporate team sizes
	+ NEW: added switch dead only mode
	+ NEW: added lots of configuration variables
	+ NEW: added team advantage announcements
	+ FIXED: trying to switch player that had already changed teams in the same round
 
	KNOWN BUGS
	----------
	- sometimes dividing 2 positive fixed numbers yields a negative number,
	  the chosen workaround might not always do it
	- playercount() doesn't always give the correct number
	- playerinfo() doesn't always give correct team
 
	SUGGESTIONS & TODO
	------------------
	- use last xxx kills as k/d stats
	- cut down kills and deaths, preserving the ratio (150/50 => 15/5)
	- ban instead of kick
	- keep spectators out of PTB_LIMITMIN and PTB_SWITCHMIN
	- configurable colors
	- configurable/translatable text messages
	- modified ying-yang logo
	- invent map type factors (de_, cs_, as_)
	- check, if switch/transfer really happened (anti-cheat against block of "kill" commands)
	(PTB 1.5 introduces new switching code, it remains to be seen,
	if this makes other potential changes obsolete)
 
*/
 
#pragma dynamic 32768 // increase to 262144 to solve stack overflow issues (error code 3)
 
#include <core>
#include <console>
#include <string>
#include <admin>
#include <adminlib>
#include <fixed>
 
new STRING_VERSION[MAX_DATA_LENGTH] = "1.7 BETA";
 
#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 = 0; // number of rounds after which teams limiting begins
new PTB_LIMITMIN = 0; // number of minimum players on map for team limiting
new PTB_MAXSIZE = 10; // maximum team size per team
new PTB_MAXDIFF = 2; // maximum team size difference
new PTB_AUTOROUNDS = 3; // number of rounds into match, which allow autojoin only
new PTB_WTJAUTO = 2; // wtj tries needed to become autojoined
new PTB_WTJKICK = 3; // 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 = 0; // 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 = 3; // number of tries after which PTB switches alive, if neccessary
new PTB_DEADONLY = 1; // switch dead only
 
// messages
new PTB_TELLWTJ = 1; // tell about wtj tries
new PTB_ANNOUNCE = 1; // announce team status at beginning of round
new PTB_SAYOK = 1; // announce team status, if teams are alright
new PTB_TYPESAY = 1; // 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; // minimum critical team rating
new fixed:PTB_MAXRATING; // maximum critical team rating
new fixed:PTB_SUPERRATING; // super critical team rating
new PTB_MAXINCIDENTS = 50; // maximum kills + deaths before the score is divided by PTB_SCALEDOWN
new PTB_SCALEDOWN = 2; // divisor for kills and deaths, when PTB_MAXINCIDENTS is reached
 
new sortedTeams[3][MAX_PLAYERS + 1];
 
new validTargetCounts[3];
// sorted player indices are 0-based
new sortedValidTargets[3][MAX_PLAYERS + 1];
 
new kills[MAX_PLAYERS + 1] = { 0, ... };
new deaths[MAX_PLAYERS + 1] = { 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 + 1];
new isBeingTransfered[MAX_PLAYERS + 1];
new playerTeam[MAX_PLAYERS + 1];
new lastRoundSwitched[MAX_PLAYERS + 1];
new wtjCount[MAX_PLAYERS + 1];
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;
}
 
/*
public admin_transfer(HLCommand, HLData, HLUserName, UserIndex) {
	new Data[MAX_DATA_LENGTH];
	convert_string(HLData, Data, MAX_DATA_LENGTH);
	new i = strtonum(Data);
	if (i >= 1 && i <= MAX_PLAYERS) transferPlayer(i);
}
 
public admin_spectator(HLCommand, HLData, HLUserName, UserIndex) {
	new Data[MAX_DATA_LENGTH];
	convert_string(HLData, Data, MAX_DATA_LENGTH);
	new i = strtonum(Data);
	if (i >= 1 && i <= MAX_PLAYERS) {
		new Name[MAX_NAME_LENGTH];
		playerinfo(i, Name, MAX_NAME_LENGTH);
		execclient(Name, "chooseteam; menuselect 6");
		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");
		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");
	}
}
*/
 
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];
	new _user, _won, _team, _dead;
	playerinfo(player, Name, MAX_NAME_LENGTH, _user, _won, _team, _dead);
 
	isBeingTransfered[player] = 1;
 
	//say("Transferring Player:");
	//say(Name);
 
	if (playerTeam[player] == TS) {
		execclient(Name, "chooseteam; menuselect 2; menuselect 5");
	}
	else {
		execclient(Name, "chooseteam; menuselect 1; menuselect 5");
	}
 
	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");
	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("PTB: Round ended, checking teams.");
 
	checkTeamBalance();
 
	if (winnerTeam != 0) {
		sortTeam(CTS);
		sortTeam(TS);
 
		if (teamCounts[winnerTeam] <= teamCounts[loserTeam]) doSwitch();
		else if (teamCounts[loserTeam] < teamCounts[winnerTeam]) doTransfer();
 
		//new text[MAX_TEXT_LENGTH];
		//snprintf(text, MAX_TEXT_LENGTH, "Winners: %d, Losers: %d", winners, losers);
		//say(text);
	}
}
 
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]])
			 || (roundCounter - lastRoundSwitched[sortedTeams[theTeam][i]] < PTB_PLAYERFREQ)
		) {
			//say("Player switched this round...")
			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 = "PTB: 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 = "PTB: 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) {
		text = "PTB: 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, "PTB: 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("PTB: Couldn't switch dead, switching alive.");
				doSwitch();
				return;
			}
			strcpy(text, "PTB: 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, "PTB: 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];
	new _user, _won, _team, _dead;
	//new debug[MAX_TEXT_LENGTH];
	//snprintf(debug, MAX_TEXT_LENGTH, "Winner Index: %d", winnerIndex);
	//say(debug);
	//snprintf(debug, MAX_TEXT_LENGTH, "Loser Index: %d", loserIndex);
	//say(debug);
 
	playerinfo(winner, winnerName, MAX_NAME_LENGTH, _user, _won, _team, _dead);
	playerinfo(loser, loserName, MAX_NAME_LENGTH, _user, _won, _team, _dead);
 
	// if one team is full, first move the the player from the full team ...
	if (teamCounts[winnerTeam] == PTB_MAXSIZE){
		transferPlayer(winner);
		transferPlayer(loser);
	}
	else {
		transferPlayer(loser);
		transferPlayer(winner);
	}
 
	strcpy(text, "PTB: 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, "PTB: 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, "PTB: 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("PTB: Couldn't transfer dead, transferring alive.");
				doTransfer();
				return;
			}
			strcpy(text, "PTB: 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, "PTB: 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];
	new _user, _won, _team, _dead;
	//new debug[MAX_TEXT_LENGTH];
	//snprintf(debug, MAX_TEXT_LENGTH, "Winner Index: %d", winnerIndex);
	//say(debug);
 
	playerinfo(winner, winnerName, MAX_NAME_LENGTH, _user, _won, _team, _dead);
 
	transferPlayer(winner);
 
	strcpy(text, "PTB: 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 _user, _won, _team, _dead;
	playerinfo(UserIndex, Name, MAX_NAME_LENGTH, _user, _won, _team, _dead);
 
	new text[MAX_TEXT_LENGTH];
	snprintf(text, MAX_TEXT_LENGTH, "%s %s <%d> %s", Time, Name, _won, Map);
 
	writefile("wtj.log", text);
}
 
public MenuSelect(HLCommand, HLData, HLUserName, UserIndex) {
	//say("MenuSelect");
 
	/*
	new s[MAX_TEXT_LENGTH];
	new d[MAX_DATA_LENGTH];
	new n[MAX_NAME_LENGTH];
	convert_string(HLData, d, MAX_DATA_LENGTH);
	convert_string(HLUserName, n, MAX_DATA_LENGTH);
	snprintf(s, MAX_TEXT_LENGTH, "%s (%d) selected menu %s (is choosing team: %d)", n, UserIndex, d, isChoosingTeam[UserIndex]);
	say(s);
	*/
	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];
	new _user, _won, _team, _dead;
	playerinfo(UserIndex, Name, MAX_NAME_LENGTH, _user, _won, _team, _dead);
 
	// prevent unwanted rejoining of the same team ...
	if (iNewTeam == playerTeam[UserIndex]) {
		//say("Preventing rejoining of the same team.");
		return PLUGIN_HANDLED;
	}
 
	checkTeamBalance();
 
	//displayStatistics(true);
 
	// debug
	//snprintf(s, MAX_TEXT_LENGTH, "iOldTeam = %d, iNewTeam = %d, winnerTeam = %d, loserTeam = %d, CTS = %d, TS = %d, AUTO_TEAM = %d, #CTs = %d, #Ts = %d", iOldTeam, iNewTeam, winnerTeam, loserTeam, CTS, TS, AUTO_TEAM, teamCounts[CTS], teamCounts[TS]);
	//say(s);
 
	// 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, "PTB: 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, "PTB: The CTs are strong enough, %s (WTJ count: %d).", Name, wtjCount[UserIndex]);
					doTypesay(text, 5, 10, 10, 255);
				}
				else {
					snprintf(text, 80, "PTB: 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("PTB: Maximum team size prohibits team change.");
			return PLUGIN_HANDLED;
		}
 
		// check team size difference limits
		if ((teamCounts[iNewTeam] + 1) - (teamCounts[opposingTeam] - 1) > PTB_MAXDIFF) {
			selfmessage("PTB: 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, "PTB: 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, "PTB: 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, "PTB: 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, "PTB: 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, "PTB: 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("PTB: Maximum team size prohibits team join.");
			return PLUGIN_HANDLED;
		}
 
		// check team size difference limits
		if ((teamCounts[iNewTeam] + 1) - (teamCounts[opposingTeam]) > PTB_MAXDIFF) {
			selfmessage("PTB: 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.");
			// debug
			//new text[MAX_TEXT_LENGTH];
			//snprintf(text, MAX_TEXT_LENGTH, "Aborting auto-joining team %d (winnerTeam = %d) teamCounts[iOldTeam] %d teamCounts[opposingTeam] %d", opposingTeam, winnerTeam, teamCounts[iOldTeam], teamCounts[opposingTeam]);
			//say(text);
 
			// close menu
			return PLUGIN_HANDLED;
		}
 
		iNewTeam = opposingTeam;
 
		// debug
		//new text[MAX_TEXT_LENGTH];
		//snprintf(text, MAX_TEXT_LENGTH, "Auto-joining team %d. (winnerTeam = %d)", iNewTeam, winnerTeam);
		//say(text);
 
		// check for maximum team size
		if (teamCounts[iNewTeam] + 1 > PTB_MAXSIZE) {
			selfmessage("PTB: 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 is the "always smaller team" version
		if (teamCounts[CTS] < teamCounts[TS] || teamCounts[TS] == PTB_MAXSIZE) iNewTeam = CTS;
		else if (teamCounts[TS] < teamCounts[CTS] || teamCounts[CTS] == PTB_MAXSIZE) iNewTeam = TS;
		// both teams have same size ...
		else if (winnerTeam != 0 && teamCounts[loserTeam] < PTB_MAXSIZE) iNewTeam = loserTeam;
		else if (teamCounts[TS] == PTB_MAXSIZE) iNewTeam = CTS;
		else if (teamCounts[CTS] == PTB_MAXSIZE) iNewTeam = TS;
		else iNewTeam = random(2) + 1;
		*/
 
		// 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;
 
		// debug
		//new text[MAX_TEXT_LENGTH];
		//snprintf(text, MAX_TEXT_LENGTH, "Auto-joining team %d. (winnerTeam = %d)", iNewTeam, winnerTeam);
		//say(text);
 
		// check for maximum team size
		if (teamCounts[iNewTeam] + 1 > PTB_MAXSIZE) {
			selfmessage("PTB: 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_teamaction(HLCommand, HLData, HLUserName, UserIndex)
{
	//say("ptb_teamaction");
 
	new params[MAX_DATA_LENGTH];
	new team[MAX_DATA_LENGTH];
	new action[MAX_DATA_LENGTH];
 
	convert_string(HLData, params, MAX_DATA_LENGTH);
 
	//log("PTB: ptb_teamaction");
	//log(params);
 
	strbreak(params, team, action, MAX_DATA_LENGTH);
 
/*
	if(
		strmatch(action, "CTs_Win", strlen("CTs_Win"))
		|| strmatch(action, "Bomb_Defused", strlen("Bomb_Defused"))
		|| strmatch(action, "Terrorists_Win", strlen("Terrorists_Win"))
		|| strmatch(action, "Target_Bombed", strlen("Target_Bombed"))
	) {
*/
	new scores[MAX_DATA_LENGTH];
	new ctScore[MAX_DATA_LENGTH];
	new tScore[MAX_DATA_LENGTH];
	new skip[MAX_DATA_LENGTH];
	new c;
 
	strbreak(action, skip, scores, MAX_DATA_LENGTH);
	strbreak(scores, ctScore, tScore, MAX_DATA_LENGTH);
 
	c = strchr(ctScore, '#') + 1;
	teamScores[CTS] = strtonum(ctScore[c]);
 
	c = strchr(tScore, '#') + 1;
	teamScores[TS] = strtonum(tScore[c]);
 
	// count won rounds in a row
/*
		if (strmatch(action, "CTs_Win", strlen("CTs_Win"))
			|| strmatch(action, "Bomb_Defused", strlen("Bomb_Defused"))
		) {
*/
	if (!betweenRounds) {
		// prevent "bomb explodes after ts win" situations
		// from producing 2 wins in the same round
		if (strmatch(team, "CT", strlen("CT"))) {
			if (winStreaks[CTS] < 0) {
				// reset counters
				winStreaks[CTS] = 1;
				winStreaks[TS] = -1;
			}
			else {
				winStreaks[CTS] += 1;
				winStreaks[TS] -= 1;
			}
		}
		else if (strmatch(team, "TERRORIST", strlen("TERRORIST"))) {
			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) {
	//say("ptb_worldaction");
 
	new Data[MAX_DATA_LENGTH];
	convert_string(HLData, Data, MAX_DATA_LENGTH);
	//say(Data);
 
	//log("PTB: 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 sIndex[MAX_DATA_LENGTH];
	new sTeam[MAX_DATA_LENGTH];
	new i;
	new team;
 
	convert_string(HLData, data, MAX_DATA_LENGTH);
 
	//log("PTB: ptb_teamselection");
	//log(data);
 
	strsplit(data, " ", sIndex, MAX_DATA_LENGTH, sTeam, MAX_DATA_LENGTH);
 
	//new debug[MAX_DATA_LENGTH];
	//snprintf(debug, MAX_DATA_LENGTH, "PTB: 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("PTB: 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 data[MAX_DATA_LENGTH];
	//new name[MAX_NAME_LENGTH];
 
	convert_string(HLData, data, MAX_DATA_LENGTH);
 
	//log("PTB: ptb_kill");
	//log(data);
 
	strsplit(data, " ", idWinner, 3, idLoser, 3);
 
	iWinner = strtonum(idWinner);
	iLoser = strtonum(idLoser);
 
	if (iWinner < 1 || iWinner > MAX_PLAYERS) return PLUGIN_CONTINUE;
	if (iLoser < 1 || iLoser > MAX_PLAYERS) return PLUGIN_CONTINUE;
 
	kills[iWinner] = kills[iWinner] + 1;
	deaths[iLoser] = deaths[iLoser] + 1;
 
	if (PTB_SCALEDOWN > 1) {
		if (kills[iWinner] + deaths[iWinner] >= PTB_MAXINCIDENTS) {
			kills[iWinner] = kills[iWinner] / PTB_SCALEDOWN;
			deaths[iWinner] = deaths[iWinner] / PTB_SCALEDOWN;
		}
		if (kills[iLoser] + deaths[iLoser] >= PTB_MAXINCIDENTS) {
			kills[iLoser] = kills[iLoser] / PTB_SCALEDOWN;
			deaths[iLoser] = deaths[iLoser] / PTB_SCALEDOWN;
		}
	}
 
	//if (playerTeam[iWinner] != CTS && playerTeam[iWinner] != TS) log("PTB: Kill without team!");
	//if (playerTeam[iLoser] != CTS && playerTeam[iLoser] != TS) log("PTB: 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, "PTB: The COUNTER-TERRORIST team could use some support.");
		doTypesay(text, 5, 10, 10, 255);
		say("PTB: The COUNTER-TERRORIST team could use some support.");
	}
	else if (winnerTeam == CTS) {
		snprintf(text, MAX_TEXT_LENGTH, "PTB: The TERRORIST team could use some support.");
		doTypesay(text, 5, 255, 10, 10);
		say("PTB: The TERRORIST team could use some support.");
	}
	else if (wtConditions[TS] > wtConditions[CTS]) {
		snprintf(text, MAX_TEXT_LENGTH, "PTB: Observing TERRORIST team advantage.");
		doTypesay(text, 5, 10, 10, 255);
		say("PTB: Observing TERRORIST team advantage.");
	}
	else if (wtConditions[CTS] > wtConditions[TS]) {
		snprintf(text, MAX_TEXT_LENGTH, "PTB: Observing COUNTER-TERRORIST team advantage.");
		doTypesay(text, 5, 255, 10, 10);
		say("PTB: Observing COUNTER-TERRORIST team advantage.");
	}
	else if (PTB_SAYOK) {
		snprintf(text, MAX_TEXT_LENGTH, "PTB: Teams look fine, no action required.");
		doTypesay(text, 5, 255, 255, 255);
		say("PTB: Teams look fine, no action required.");
	}
 
	//displayStatistics();
}
 
public admin_ptb(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);
 
	snprintf(s, MAX_TEXT_LENGTH, "PTB: Ptahhotep's Team Balancer %s", STRING_VERSION);
	selfmessage(s);
	selfmessage("PTB: (ptahhotep@planethalflife.com)");
 
	if (!strlen(Data)) {
		checkTeamBalance();
		displayStatistics();
		return PLUGIN_HANDLED;
	}
	else if (streq(Data, "on") || streq(Data, "1")) {
		if (!access(ACCESS_BALANCE, "")) {
			selfmessage("PTB: You are not allowed to use this command.");
			return PLUGIN_HANDLED;
		}
		PTB_LIMITJOIN = 1;
		PTB_SWITCH = 1;
		PTB_ANNOUNCE = 1;
		selfmessage("PTB: Enabled all PTB actions.");
		return PLUGIN_HANDLED;
	}
	else if (streq(Data, "off") || streq(Data, "0")) {
		if (!access(ACCESS_BALANCE, "")) {
			selfmessage("PTB: You are not allowed to use this command.");
			return PLUGIN_HANDLED;
		}
		PTB_SWITCH = 0;
		PTB_ANNOUNCE = 0;
		PTB_LIMITJOIN = 0;
		selfmessage("PTB: Disabled all PTB actions.");
		return PLUGIN_HANDLED;
	}
	else if (streq(Data, "save")) {
		if (!access(ACCESS_BALANCE, "")) {
			selfmessage("PTB: You are not allowed to use this command.");
			return PLUGIN_HANDLED;
		}
		saveSettings();
		selfmessage("PTB: Saved all PTB settings to ^"vault.ini^".");
		return PLUGIN_HANDLED;
	}
	else if (streq(Data, "load")) {
		if (!access(ACCESS_BALANCE, "")) {
			selfmessage("PTB: You are not allowed to use this command.");
			return PLUGIN_HANDLED;
		}
		loadSettings();
		selfmessage("PTB: Loaded all PTB settings from ^"vault.ini^".");
		return PLUGIN_HANDLED;
	}
	else if (streq(Data, "list") || streq(Data, "help")) {
		selfmessage("PTB: Available Commands:");
		selfmessage("PTB: Team Join Control: ^"limitjoin^", ^"limitafter^", ^"limitmin^", ^"maxsize^", ^"autorounds^",");
		selfmessage("PTB: ^"maxdiff^", ^"wtjauto^", ^"wtjkick^", ^"kick^", ^"savewtj^"");
		selfmessage("PTB: Team Balancing Actions: ^"switch^", ^"switchafter^", ^"switchmin^", ^"switchfreq^", ^"playerfreq^",");
		selfmessage("PTB: ^"forceswitch^", ^"deadonly^"");
		selfmessage("PTB: Team Strength Limits: ^"maxstreak^", ^"maxscore^", ^"minrating^", ^"maxrating^", ^"superrating^"");
		selfmessage("PTB: Messages: ^"tellwtj^", ^"announce^", ^"sayok^", ^"typesay^"");
		selfmessage("PTB: Misc: ^"^", ^"status^", ^"list^", ^"help^", ^"on^", ^"off^", ^"save^", ^"load^"");
		selfmessage("PTB: To view all PTB settings, type ^"admin_ptb status^".");
		selfmessage("PTB: To view or change a single PTB setting, type ^"admin_ptb <setting> <^"on^"|^"off^"|value>^".");
		selfmessage("PTB: For PTB statistics, simply type ^"admin_ptb^".");
		return PLUGIN_HANDLED;
	}
	else {
		strsplit(Data, " ", Command, MAX_DATA_LENGTH, Argument, MAX_DATA_LENGTH);
		if (strlen(Argument) && !access(ACCESS_BALANCE, "")) {
			selfmessage("PTB: You are not allowed to use this command.");
			return PLUGIN_HANDLED;
		}
	}
 
 
	// team selection control
	if (streq(Command, "status")) selfmessage("PTB: ---------- 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("PTB: (limitjoin) WTJ prevention is ON."); else selfmessage("PTB: (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, "PTB: (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, "PTB: (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, "PTB: (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, "PTB: (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, "PTB: (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, "PTB: (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, "PTB: (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("PTB: (kick) WTJ kicking is ON."); else selfmessage("PTB: (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("PTB: (savewtj) Saving to wtj.txt is ON."); else selfmessage("PTB: (savewtj) Saving to wtj.txt is OFF."); }
 
 
	// team balancing actions
	if (streq(Command, "status")) selfmessage("PTB: ---------- 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("PTB: (switch) Team switching is ON."); else selfmessage("PTB: (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, "PTB: (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, "PTB: (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, "PTB: (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, "PTB: (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, "PTB: (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("PTB: (deadonly) Switching dead only is ON."); else selfmessage("PTB: (deadonly) Switching dead only is OFF."); }
 
 
	// messages
	if (streq(Command, "status")) selfmessage("PTB: ---------- 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("PTB: (tellwtj) Telling about WTJ tries is ON."); else selfmessage("PTB: (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("PTB: (announce) Announcements are ON."); else selfmessage("PTB: (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("PTB: (sayok) ^"OK^" announcements are ON."); else selfmessage("PTB: (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("PTB: (typesay) typesay usage is ON."); else selfmessage("PTB: (typesay) typesay usage is OFF."); }
 
 
	// team strength limits
	if (streq(Command, "status")) selfmessage("PTB: ---------- 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, "PTB: (maxstreak) Maximum accepted win streak is %d.", PTB_MAXSTREAK);
		selfmessage(s);
	}
 
	// PTB_MAXSCORE
	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, "PTB: (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, "PTB: (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, "PTB: (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, "PTB: (superrating) Super critical strength rating is %d.%d.",
			fround(PTB_SUPERRATING, fround_floor),
			fround(fmul(fixed(10), ffract(PTB_SUPERRATING)), fround_floor)
		);
		selfmessage(s);
	}
 
	// PTB_MAXINCIDENTS
	if (streq(Command, "maxincidents") && strlen(Argument)) {
		PTB_MAXINCIDENTS = strtonum(Argument);
		if (PTB_MAXINCIDENTS < 1) PTB_MAXINCIDENTS = 1;
	}
	if (streq(Command, "status") || streq(Command, "maxincidents")) {
		snprintf(s, MAX_TEXT_LENGTH, "PTB: (maxincidents) Maximum incidents before internal player score scale down is %d.", PTB_MAXINCIDENTS);
		selfmessage(s);
	}
 
	// PTB_SCALEDOWN
	if (streq(Command, "scaledown") && strlen(Argument)) {
		PTB_SCALEDOWN = strtonum(Argument);
		if (PTB_SCALEDOWN < 1) PTB_SCALEDOWN = 1;
	}
	if (streq(Command, "status") || streq(Command, "scaledown")) {
		snprintf(s, MAX_TEXT_LENGTH, "PTB: (scaledown) Integer scale down factor for player scores is %d.", PTB_SCALEDOWN);
		selfmessage(s);
	}
	// misc
	if (streq(Command, "status")) {
		selfmessage("PTB: ---------- Misc ----------");
		selfmessage("PTB: To enable or disable PTB, type ^"admin_ptb <^"on^"|^"1^"|^"off^"|^"0^">^".");
		selfmessage("PTB: To view or change a single PTB setting, type ^"admin_ptb <setting> <^"on^"|^"off^"|value>^".");
		selfmessage("PTB: To view a brief overview of PTB commands, type ^"admin_ptb help^" or ^"admin_ptb list^".");
		selfmessage("PTB: To save all PTB settings to ^"vault.ini^", type ^"admin_ptb save^".");
		selfmessage("PTB: To load all PTB settings from ^"vault.ini^", type ^"admin_ptb load^".");
		selfmessage("PTB: For PTB statistics, simply type ^"admin_ptb^".");
	}
 
	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);
	snprintf(VAULTDATA, MAX_DATA_LENGTH, "%d", PTB_MAXINCIDENTS);
	set_vaultdata("PTB_MAXINCIDENTS", VAULTDATA);
	snprintf(VAULTDATA, MAX_DATA_LENGTH, "%d", PTB_SCALEDOWN);
	set_vaultdata("PTB_SCALEDOWN", 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);
	if (get_vaultdata("PTB_MAXINCIDENTS", VAULTDATA, MAX_DATA_LENGTH ) != 0)
		PTB_MAXINCIDENTS = strtonum(VAULTDATA);
	if (get_vaultdata("PTB_SCALEDOWN", VAULTDATA, MAX_DATA_LENGTH ) != 0)
		PTB_SCALEDOWN = strtonum(VAULTDATA);
}
 
stock displayStatistics(toLog = false) {
	//say("displayStatistics");
 
	new text[MAX_TEXT_LENGTH];
 
	// time
	snprintf(text, MAX_TEXT_LENGTH, "PTB: Statistics generated at: %s", lastTeamBalanceCheck);
	if (toLog) log(text);
	else selfmessage(text);
 
	// connected players
	snprintf(text, MAX_TEXT_LENGTH, "PTB: Connected players: %d", playercount());
	if (toLog) log(text);
	else selfmessage(text);
 
	// team sizes
	snprintf(text, MAX_TEXT_LENGTH, "PTB: Team sizes: CTs %d, Ts %d", teamCounts[CTS], teamCounts[TS]);
	if (toLog) log(text);
	else selfmessage(text);
 
	// team scores
	snprintf(text, MAX_TEXT_LENGTH, "PTB: Team scores: CTs %d, Ts %d", teamScores[CTS], teamScores[TS]);
	if (toLog) log(text);
	else selfmessage(text);
 
	// Kills:Deaths
	snprintf(text, MAX_TEXT_LENGTH, "PTB: 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, "PTB: 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, "PTB: 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, "PTB: 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, "PTB: 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, "PTB: 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, "PTB: The CTs are the winning team.");
	else if (winnerTeam == TS) snprintf(text, MAX_TEXT_LENGTH, "PTB: The Ts are the winning team.");
	else snprintf(text, MAX_TEXT_LENGTH, "PTB: Teams are balanced.");
	if (toLog) log(text);
	else selfmessage(text);
 
	snprintf(text, MAX_TEXT_LENGTH, "PTB: (These statistics might be already outdated.)");
	if (toLog) log(text);
	else selfmessage(text);
 
	snprintf(text, MAX_TEXT_LENGTH, "PTB: To view a brief overview of PTB commands, type ^"admin_ptb help^" or ^"admin_ptb list^".");
	if (toLog) log(text);
	else selfmessage(text);
 
	snprintf(text, MAX_TEXT_LENGTH, "PTB: To view all PTB settings, type ^"admin_ptb 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("PTB: 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] PTB 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("PTB: 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()
{
	//say("plugin_init");
 
	//initVariables();
 
	// must du this here
	PTB_MINRATING = fixedstr(MIN_RATING);
	PTB_MAXRATING = fixedstr(MAX_RATING);
	PTB_SUPERRATING = fixedstr(SUPER_RATING);
 
	plugin_registerinfo("PTB", "Ptahhotep's Team Balancer", STRING_VERSION);
	plugin_registercmd("admin_ptb", "admin_ptb", ACCESS_ALL, "admin_ptb <^"on^" | ^"off^" | ^"status^" | ^"^">: Switch Ptahhotep's Team Balancer on or off, get status or statistics.");
 
	//plugin_registercmd("admin_transfer", "admin_transfer", ACCESS_BALANCE);
	//plugin_registercmd("admin_spectator", "admin_spectator", ACCESS_BALANCE);
 
	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);
 
	exec("logd_reg 54 admin_command ptb_teamselection");
	exec("logd_reg 57 admin_command ptb_kill");
	exec("logd_reg 61 admin_command ptb_teamaction");
	exec("logd_reg 62 admin_command ptb_worldaction");
 
	loadSettings();
 
	return PLUGIN_CONTINUE;
}