//============================================================
// TTeamFixGeneric.uc	- Gametype independant team balancing class, the actual balancer is implemented here
//============================================================
//	TitanTeamFix
//		- A modular team balancing tool initially coded for the Titan servers:
//			http://ut2004.titaninternet.co.uk/
//
//	Copyright (C) 2007-2008 John "Shambler" Barrett (JBarrett847@Gmail.com or Shambler@OldUnreal.com)
//
//	This program is free software; you can redistribute and/or modify
//	it under the terms of the Open Unreal Mod License version 1.1.
//
//============================================================
//
// Highly configurable team balancer class.
// It's possible to tweak this to suit a variety of different
// gametypes, all without touching any code.
//
//============================================================
Class TTeamFixGeneric extends TTeamFix;


// ===== Variables set by the config profile

// The course of action to take when the teams have become uneven
enum EImbalanceAction
{
	IA_NoAction,			// For when you want no automatic balancing
	IA_Countdown,			// Counts down from 'BalanceCountdown' seconds and if the teams are still uneven, it forcibly rebalances the teams
	IA_DeathEvent,			// Monitors players that have been ingame for less than two minutes, and switches any that die until the teams balance
	IA_CountdownOrDeath,		// A combination of IE_Countdown and IE_DeathEvent
	IA_TeamDeath			// Monitors for the death of an entire team and then forcibly rebalances the teams (for round based gametypes)
};

enum EPreferredTeams
{
	PT_Disable,
	PT_Enable,
	PT_PreferLosingTeam
};

enum EShuffleTeams
{
	ST_Disable,
	ST_MatchStart,
	ST_MatchEnd
};

var bool bCheckTeamsOnExit;		// If true then the code checks if the teams are uneven each time someone exits, and then acts according to ExitEvent
// REMOVED, treat as if true
//var bool bSpectateMeansExit;		// If true then when a player becomes a spectator then he is treated as an exiting player

var bool bCheckTeamsOnDeath;		// As above except only applies to dying players

var EImbalanceAction ImbalanceAction;	// The action to undertake when the teams become uneven
var int BalanceCountdown;		// If ImbalanceAction is set to 'IA_Countdown' or 'IA_CountdownOrDeath' then this is the countdown time

// Variables related to messages
var bool bAnnounceSwitch;		// Tells all the players that 'blah' has been switched
var bool bAnnounceImbalance;		// When the teams become imbalanced, notify all players
var bool bAnnounceSlotOpened;		// When a server is full and an exiting player opens a slot, notify all spectators

var EPreferredTeams PreferredTeams;	// Preferred team settings, allows the server to disable preferred teams or set players to join the losing team
var EShuffleTeams ShuffleTeams;		// Shuffe teams settings, allows the server to shuffle the teams at the start of a match or at the end

//var bool bShuffleTeamsOnTravel;		// If true, the teams are randomized when the server begins seamless-travelling

var bool bBotsBalanceTeams;		// If true, bots are used to keep the teams balanced
var int BotBalanceCutoff;		// If 'bBotsBalanceTeams', then bots are no longer used to balance the teams when the playercount exceeds this number

// =====

var bool bPendingBalance;		// If true, the auto-balance countdown is active and/or the code is waiting for a recently joined player to die and switch
var int MinPlayers;			// If 'bBotsBalanceTeams', then this is set to match the gameinfo's DesiredPlayerCount

// Variables used as compiler messages
var deprecated bool bTODO;
var deprecated bool bTestThisCode;


// ===== General functions
function InitializeBalancing()
{
	Super.InitializeBalancing();

	if (bBotsBalanceTeams && !UTGame(WorldInfo.Game).bPlayersVsBots)
	{
		// Temporarily disable bBotsBalanceTeams, since this is called before DesiredPlayerCount is set and must be delayed by a tick
		bBotsBalanceTeams = False;
		SetTimer(0.001, False, 'InitializeMinPlayers'); // Calls 'InitializeMinPlayers' during the next tick

/*
		MinPlayers = UTGame(WorldInfo.Game).DesiredPlayerCount;


		// Upscale MinPlayers if it isn't an even number
		MinPlayers += MinPlayers % 2;

		// Downscale MinPlayers if it exceeds MaxPlayers
		if (MinPlayers > WorldInfo.Game.MaxPlayers)
			MinPlayers -= 2;


		UTGame(WorldInfo.Game).DesiredPlayerCount = MinPlayers;
*/
	}
}

// Called first-tick after InitGame, so that I can grab UTGame.DesiredPlayerCount
function InitializeMinPlayers()
{
	MinPlayers = UTGame(WorldInfo.Game).DesiredPlayerCount;


	// Upscale MinPlayers if it isn't an even number
	MinPlayers += MinPlayers % 2;

	// Downscale MinPlayers if it exceeds MaxPlayers
	if (MinPlayers > WorldInfo.Game.MaxPlayers)
		MinPlayers -= 2;


	UTGame(WorldInfo.Game).DesiredPlayerCount = (BotBalanceCutoff == 0 ? MinPlayers : BotBalanceCutoff);


	bBotsBalanceTeams = True;
	ClearTimer('InitializeMinPlayers');
}

function InitializeDefaults()
{
	// ***
	bTODO = True;
	// It appears that UT3 allows you to switch teams, even if you imbalance the teams by switching; find an efficient way to block/revert that
	// ***

	if (bBotsBalanceTeams)
		bNeedJoinedEvent = True;

	if (bCheckTeamsOnExit || bAnnounceSlotOpened || bBotsBalanceTeams)
		bNeedExitingEvent = True;

	if (PreferredTeams == PT_Disable || PreferredTeams == PT_PreferLosingTeam)
		bNeedJoiningEvent = True;

	bTODO = True;
	// When player->spectator is added back into UT, re-add bAnnounceSlotOpened (assuming you're going to use PlayerKilled to detect spectators again)
	if (bCheckTeamsOnDeath /*|| bAnnounceSlotOpened*/)
		bNeedKilledEvent = True;
}

// Implements team-shuffling on server travel
/*
function ResetBalancing()
{
	local PlayerController PC;
	local int BiggerTeam, Imbalance;
	local array<PlayerController> RebalanceList;

	Super.ResetBalancing();


	if (bShuffleTeamsOnTravel)
	{
		// Randomize the teams
		foreach WorldInfo.AllControllers(Class'PlayerController', PC)
			if (PC.PlayerReplicationInfo != none && !PC.PlayerReplicationInfo.bOnlySpectator && Rand(2) != PC.GetTeamNum())
				TTFChangeTeam(PC, int(!bool(PC.GetTeamNum())));


		// If the teams were unbalanced through randomization, fix that
		if (bTeamsUneven(BiggerTeam, Imbalance, True))
		{
			foreach WorldInfo.AllControllers(Class'PlayerController', PC)
				if (PC.GetTeamNum() == BiggerTeam && PC.PlayerReplicationInfo != none && !PC.PlayerReplicationInfo.bOnlySpectator)
					RebalanceList.InsertItem(0, PC);

			// Decide upon the number of players to switch, (Imbalance & 0xFFFE) cuts odd numbers, turning 3 into 2, 5 into 4 etc.
			RebalanceList.Length = (Imbalance & 0xFFFE) / 2;
			BiggerTeam = int(!bool(BiggerTeam));

			foreach RebalanceList(PC)
				TTFChangeTeam(PC, BiggerTeam);
		}
	}
}
*/

function ResetBalancing()
{
	Super.ResetBalancing();

	if (ShuffleTeams == ST_MatchEnd)
		RandomizeTeams();
}

function MatchStarting()
{
	//local PlayerController PC;
	local int BiggerTeam, Imbalance;
	//local array<PlayerController> RebalanceList;

	// ***
	bTestThisCode = True;
	// This code shuffles the teams AFTER server travel, but it may have unforseen bugs. If it works, remove the commented code above
	// ***

	// Implements team-shuffling on server travel
	//if (bShuffleTeamsOnTravel)
	if (ShuffleTeams == ST_MatchStart)
	{
/*
		// Randomize the teams
		foreach WorldInfo.AllControllers(Class'PlayerController', PC)
			if (PC.PlayerReplicationInfo != none && !PC.PlayerReplicationInfo.bOnlySpectator && Rand(2) != PC.GetTeamNum())
				TTFChangeTeam(PC, int(!bool(PC.GetTeamNum())));


		// If the teams were unbalanced through randomization, fix that
		if (bTeamsUneven(BiggerTeam, Imbalance))
		{
			foreach WorldInfo.AllControllers(Class'PlayerController', PC)
				if (PC.GetTeamNum() == BiggerTeam && PC.PlayerReplicationInfo != none && !PC.PlayerReplicationInfo.bOnlySpectator)
					RebalanceList.InsertItem(0, PC);

			// Decide upon the number of players to switch, (Imbalance & 0xFFFE) cuts odd numbers, turning 3 into 2, 5 into 4 etc.
			RebalanceList.Length = (Imbalance & 0xFFFE) / 2;
			BiggerTeam = int(!bool(BiggerTeam));

			foreach RebalanceList(PC)
				TTFChangeTeam(PC, BiggerTeam);

			// Notify all players that the teams have been randomized
			if (bAnnounceSwitch)
				foreach WorldInfo.AllControllers(Class'PlayerController', PC)
					PC.ReceiveLocalizedMessage(Class'TTFMsg.TTeamFixMessages', 5);
		}
*/
		RandomizeTeams();
	}
	else if (ImbalanceAction != IA_NoAction)
	{
		// If balancing is enabled (and 'shuffle teams' disabled), check that the teams are balanced at match start
		if (bTeamsUneven(BiggerTeam, Imbalance))
			EvenTeams(BiggerTeam, Imbalance);
	}
}

function RandomizeTeams()
{
	local PlayerController PC;
	local int BiggerTeam, Imbalance;
	local array<PlayerController> RebalanceList;
	local AIController AC;
	local array<AIController> BotList;
	local bool OldbBotsBalanceTeams;
	local int OldDesiredPlayerCount;

	foreach WorldInfo.AllControllers(Class'PlayerController', PC)
		if (PC.PlayerReplicationInfo != none && !PC.PlayerReplicationInfo.bOnlySpectator && Rand(2) != PC.GetTeamNum())
			TTFChangeTeam(PC, int(!bool(PC.GetTeamNum())));


	// If the teams were unbalanced through randomization, fix that
	if (bTeamsUneven(BiggerTeam, Imbalance, True))
	{
		foreach WorldInfo.AllControllers(Class'PlayerController', PC)
			if (PC.GetTeamNum() == BiggerTeam && PC.PlayerReplicationInfo != none && !PC.PlayerReplicationInfo.bOnlySpectator)
				RebalanceList.InsertItem(0, PC);

		// Decide upon the number of players to switch, (Imbalance & 0xFFFE) cuts odd numbers, turning 3 into 2, 5 into 4 etc.
		RebalanceList.Length = (Imbalance & 0xFFFE) / 2;
		BiggerTeam = int(!bool(BiggerTeam));

		foreach RebalanceList(PC)
			TTFChangeTeam(PC, BiggerTeam);

		// Notify all players that the teams have been randomized
		if (bAnnounceSwitch)
			foreach WorldInfo.AllControllers(Class'PlayerController', PC)
				PC.ReceiveLocalizedMessage(Class'TTFMsg.TTeamFixMessages', 5);
	}


	// Seems to be causing more bugs than it fixes
	bTODO = True;


	if (ShuffleTeams != ST_MatchEnd || !UTTeamGame(WorldInfo.Game).bPlayersBalanceTeams)
	{
		// If there are any bots present, then the reshuffling code may have imbalanced the teams due to ignoring the bots.
		// To fix this, destroy the bots; UT should automatically re-add them as they get destroyed, distributing them so as to rebalance the teams

		// Temporarily disable 'bBotsBalanceTeams' and 'DesiredPlayerCount', so that UT does not immediately re-add the bots (this caused a bug
		//	where all the bots would be the default character)
		// ***
		// UPDATE: Seems to have no effect? (but keep it in for now anyway)
		bTODO = True;
		// ***
		OldbBotsBalanceTeams = bBotsBalanceTeams;
		OldDesiredPlayerCount = UTGame(WorldInfo.Game).DesiredPlayerCount;
		bBotsBalanceTeams = False;
		UTGame(WorldInfo.Game).DesiredPlayerCount = 0;


		foreach WorldInfo.AllControllers(Class'AIController', AC)
			BotList.AddItem(AC);


		// I assign bots to 'BotList' before destroying them, because destroying them within the above iterator may result in an infinite loop,
		// as UT will automatically add new bots as I destroy the old ones
		foreach BotList(AC)
		{
			if (AC.Pawn != none)
				AC.Pawn.Destroy();

			if (AC != none)
				AC.Destroy();
		}


		// Restore values
		bBotsBalanceTeams = OldbBotsBalanceTeams;
		UTGame(WorldInfo.Game).DesiredPlayerCount = OldDesiredPlayerCount;
	}
}


// Adjusts incoming players preferred team
function PlayerJoiningGame(out string Portal, out string Options)
{
	local int ScoreDiff, i, j;
	local string NewOpt;

	// Adjust the players preferred team, if appropriate
	if (PreferredTeams == PT_PreferLosingTeam)
	{
		if (GRI == none)
			GRI = WorldInfo.Game.GameReplicationInfo;

		// First determine the losing team
		ScoreDiff = GRI.Teams[0].Score - GRI.Teams[1].Score;

		if (ScoreDiff < 0)
			ScoreDiff = 0;
		else if (ScoreDiff > 0)
			ScoreDiff = 1;
		else
			ScoreDiff = Rand(2); // N.B. Rand values must always be +1 over desired max value..


		// Apply the selected team to the preffered team parameter
		i = InStr(Caps(Options), "?TEAM=");
		NewOpt = Mid(Options, i+1);
		j = InStr(NewOpt, "?");

		if (j != -1)
			NewOpt = Left(Options, i)$"?Team="$ScoreDiff$Mid(NewOpt, j);
		else
			NewOpt = Left(Options, i)$"?Team="$ScoreDiff;

		Options = NewOpt;
	}
	else if (PreferredTeams == PT_Disable)
	{
		i = InStr(Caps(Options), "?TEAM=");
		NewOpt = Mid(Options, i+1);
		j = InStr(NewOpt, "?");

		if (j != -1)
			NewOpt = Left(Options, i)$Mid(NewOpt, j);
		else
			NewOpt = Left(Options, i);

		Options = NewOpt;
	}
}

// Checks that incoming players go to the smaller team, and that bots don't imbalance the teams
// NOTE: If you uncomment this, make sure to also uncomment bNeedJoinedEvent = True in 'InitializeDefaults'
/*
function PlayerJoinedGame(Controller Player, optional bool bInvalidId)
{
	if (!WorldInfo.Game.bWaitingToStartMatch)
	{
	}
}
*/

// Currently used to help make bots balance the teams
function PlayerJoinedGame(Controller Player)
{
	if (bBotsBalanceTeams && PlayerController(Player) != none && (WorldInfo.Game.GetNumPlayers() % 2) != 0)
	{
		UTGame(WorldInfo.Game).DesiredPlayerCount = Min(Max(MinPlayers, WorldInfo.Game.GetNumPlayers() + 1),
								(BotBalanceCutoff == 0 ? WorldInfo.Game.MaxPlayers : BotBalanceCutoff));
	}
}

// N.B. Remember that this gets called for exiting players BEFORE the team sizes have changed
// TODO: Perhaps remove 'singular' and add a selective bSingular variable which is set when SwitchPlayer or EvenTeams is called? (hacky)
singular function PlayerKilled(controller Player)
{
	local int BiggerTeam, Imbalance;
	local array<PlayerController> PC;

	if (bCheckTeamsOnDeath)
	{
		// Update this to work with bots too
		if (bTeamsUneven(BiggerTeam, Imbalance))
		{
			if (bPendingBalance)
			{
				if (ImbalanceAction == IA_TeamDeath)
				{
					if (CheckTeamDeath(Player))
						EvenTeams(BiggerTeam, Imbalance);
				}
				else if (ImbalanceAction == IA_DeathEvent || ImbalanceAction == IA_CountdownOrDeath)
				{
					// Keep this if statement seperate in case I decide to add in more death related events
					if (PlayerController(Player) != none && Player.GetTeamNum() == BiggerTeam && bRecentlyJoined(Player))
					{
						// I would 'like' to handle this entirely from within TTeamFix at some stage instead of resorting to this
						PC.AddItem(PlayerController(Player));
						NotifySwitchPlayers(PC);

						SwitchPlayer(PlayerController(Player));

						if (!bTeamsUneven(BiggerTeam, Imbalance))
							SetPendingBalance(False);
					}
				}
			}
			else
			{
				NotifyImbalance();
			}
		}
		else if (bPendingBalance)
		{
			SetPendingBalance(False);
		}
	}
}

function PlayerExitingGame(Controller Player)
{
	local int BiggerTeam, Imbalance;
	local PlayerController PC;

	if (bBotsBalanceTeams)
	{
		if ((WorldInfo.Game.GetNumPlayers() % 2) != 0)
		{
			UTGame(WorldInfo.Game).DesiredPlayerCount = Min(Max(MinPlayers, WorldInfo.Game.GetNumPlayers() + 1),
									(BotBalanceCutoff == 0 ? WorldInfo.Game.MaxPlayers : BotBalanceCutoff));
		}
		else
		{
			UTGame(WorldInfo.Game).DesiredPlayerCount = Min(Max(MinPlayers, WorldInfo.Game.GetNumPlayers()),
									(BotBalanceCutoff == 0 ? WorldInfo.Game.MaxPlayers : BotBalanceCutoff));
		}
	}

	if (bCheckTeamsOnExit)
	{
		// NOTE: You don't need to add IA_TeamDeath checks here, because exiting players trigger 'PlayerKilled' before this
		if (bTeamsUneven(BiggerTeam, Imbalance))
		{
			if (!bPendingBalance)
				NotifyImbalance();
		}
		else if (bPendingBalance)
		{
			SetPendingBalance(False);
		}
	}

	// ***
	bTestThisCode = True;
	// The bAnnounceSlotOpened code is untested
	// ***

	if (bAnnounceSlotOpened && PlayerController(Player) != none && WorldInfo.Game.GetNumPlayers() + 1 >= WorldInfo.Game.MaxPlayers)
		foreach WorldInfo.AllControllers(Class'PlayerController', PC)
			if (PC.PlayerReplicationInfo != none && PC.PlayerReplicationInfo.bOnlySpectator)
				PC.ReceiveLocalizedMessage(Class'TTFMsg.TTeamFixMessages', 4);
}

// Only notifies when bPendingBalance was false
function NotifyImbalance()
{
	local PlayerController PC;

	if (bAnnounceImbalance)
		foreach WorldInfo.AllControllers(Class'PlayerController', PC)
			PC.ReceiveLocalizedMessage(Class'TTFMsg.TTeamFixMessages', 3);


	switch (ImbalanceAction)
	{
	case IA_Countdown:
	case IA_CountdownOrDeath:
		SetPendingBalance(True, True);
		break;

	case IA_DeathEvent:
	case IA_TeamDeath:
		SetPendingBalance(True);
		break;

	default:
	}
}


function SetPendingBalance(bool bPending, optional bool bEnableCountdown)
{
	bPendingBalance = bPending;

	if (bPending)
	{
		if (bEnableCountdown && !IsTimerActive('CountdownTimer'))
			SetTimer(BalanceCountdown,, 'CountdownTimer');
	}
	else
	{
		ClearTimer('CountdownTimer');
	}
}

function CountdownTimer()
{
	local int BiggerTeam, Imbalance;

	if (bTeamsUneven(BiggerTeam, Imbalance))
		EvenTeams(BiggerTeam, Imbalance);

	SetPendingBalance(False);
}

function NotifySwitchPlayers(array<PlayerController> Players)
{
	local PlayerController PC;
	local int Indicies;

	if (bAnnounceSwitch)
	{
		// Notify the players that are being switched
		foreach Players(PC)
			PC.ReceiveLocalizedMessage(Class'TTFMsg.TTeamFixMessages', 0);


		// Make a general announcement to all other players
		if (Players.Length == 1)
		{
			foreach WorldInfo.AllControllers(Class'PlayerController', PC)
				if (Players.Find(PC) == -1)
					PC.ReceiveLocalizedMessage(Class'TTFMsg.TTeamFixMessages', 1, Players[0].PlayerReplicationInfo);
		}
		else
		{
			Indicies = -1;

			foreach WorldInfo.AllControllers(Class'PlayerController', PC)
			{
				// Removed, if more than one player is switched then send this msg to those players as well, so they know who else has been switched
				//if (Players.Find(PC) != -1)
				//	continue;

				if (Indicies == -1)
					Indicies = EncodeMultipleAnnounce(Players);

				PC.ReceiveLocalizedMessage(Class'TTFMsg.TTeamFixMessages', Indicies, Players[0].PlayerReplicationInfo,, WorldInfo.Game.GameReplicationInfo);
			}
		}
	}
}

// Special function for squeezing in extra data to the 'Switch' parameter in ReceiveLocalizedMessage
final function int EncodeMultipleAnnounce(array<PlayerController> Players)
{
	local int CurTeam, i, ReturnVal;
	local PlayerReplicationInfo PRI, PRI2;
	local array<PlayerReplicationInfo> PRIList;

	// ReturnVal represents the final 'Switch' value; which must be 2 for MultipleAnnounce messages
	ReturnVal = 2;

	CurTeam = Players[0].GetTeamNum();

	// Generate the list of players from the bigger team and sort by player id
	foreach WorldInfo.Game.GameReplicationInfo.PRIArray(PRI)
	{
		if (PRI.GetTeamNum() != CurTeam)
			continue;

		if (PRIList.Length == 0)
		{
			PRIList.AddItem(PRI);
		}
		else
		{
			foreach PRIList(PRI2, i)
			{
				if (PRI.PlayerID < PRI2.PlayerID)
				{
					PRIList.InsertItem(i, PRI);
					break;
				}
			}

			if (PRIList[i] != PRI)
				PRIList.AddItem(PRI);
		}
	}

	// Now compare entries in the list to the 'Players' array and note them in the return value
	foreach PRIList(PRI, i)
		if (PlayerController(PRI.Owner) != none && Players.Find(PlayerController(PRI.Owner)) != -1)
			ReturnVal = ReturnVal | (0x8 << i);


	return ReturnVal;
}


defaultproperties
{
	ConfigProfileClass=Class'TTFProfile_Generic'
}