//============================================================
// TTeamFix.uc	- This class will be the placeholder for the balancing system
//============================================================
//	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.
//
//============================================================
//
// This class is designed to be a modular base class for all team-balancing components,
// it is designed around certain key events like for a player joining a team or leaving
// the game etc. and these key events are called by either the 'EventMutator' or the
// 'EventRules' objects.
//
// The reason I want this to be modular is so that anybody can code plugins for this
// mutator so it can run better on different gametypes.
//
//============================================================
Class TTeamFix extends Info;


// Change these values in subclasses via the default properties, in most cases the original defaults will suit your purposes
var class<TTeamFixMut>			EventMutClass;
var class<TTeamFixRules>		EventRulesClass;
var class<TTeamFixConfigProfile>	ConfigProfileClass;	// The config profile class which corresponds to this teamfix class


// Events that require 'EventRules' (set these in InitializeDefaults)
var bool				bNeedPreSpawnEvent;
var bool				bNeedKilledEvent;

// Events that require 'EventMutator'
var bool				bNeedExitingEvent;
var bool				bNeedJoiningEvent;
var bool				bNeedJoinedEvent;
var bool				bNeedDelayedJoinedEvent;	// A version of the 'PlayerJoinedGame' event which is delayed for 10 seconds or until the players ID has been retrieved


// ===== Runtime variables
var TTeamFixMut				EventMutator;
var TTeamFixRules			EventRules;
var TTeamFixConfigProfile		ConfigObject;		// Object which contains the config profile for this class

var GameReplicationInfo			GRI;
var array<Controller> 			ExitingPlayers;
var int					RecentCutoffTime;

struct SwitchList
{
	var PlayerController Player;
	var byte SwitchCount;
};

var array<SwitchList>			SwitchHistory;


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



// ===== Functions relying on outside-class events from 'EventMutator' and 'EventRules'

// 'EventRules' events
function PlayerSpawning(controller Player, out byte InTeam);
singular function PlayerKilled(controller Player);


// Removed as the UT3 WebAdmin breaks this hack
/*
function bool NotifyRestartGame()
{
	local string NextMap, TravelStr, MutStr;
	local GameInfo GI;

	// Massive hack *cringe*, hijacking the code from GameInfo::RestartGame in order to force the mutator into the URL

	// This deprecated debug msg is here to remind me to check the latest patch code to see if RestartGame has changed
	bCheckAgainstPatch_LastVer_Patch1 = True;


	GI = WorldInfo.Game;

	if (GI.bGameRestarted)
		return True;

	GI.bGameRestarted = True;

	if (GI.bChangeLevels && !GI.bAlreadyChanged)
	{
		GI.bAlreadyChanged = True;

		NextMap = GI.GetNextMap();

		if (NextMap != "")
			TravelStr = NextMap;
	}

	if (TravelStr == "")
		TravelStr = "?Restart";



	// When supplying new "?Mutator=" options to the URL, the original from the commandline is overriden. Fix this
	MutStr = GI.ParseOption(GI.ServerOptions, "Mutator");


	// Make sure the mutator gets added during the next game; the URL must be modified so that it's added during InitGame
	TravelStr $= "?Mutator=";

	if (MutStr == "")
		TravelStr $= PathName(EventMutClass);
	else if (InStr(Caps(MutStr), Caps(PathName(EventMutClass))) == -1)
		TravelStr $= MutStr$","$PathName(EventMutClass);
	else
		TravelStr $= MutStr;



	WorldInfo.ServerTravel(TravelStr, GI.GetTravelType());


	return True;
}
*/

// 'EventMutator' events

// N.B. 'PlayerKilled' is called just before this when the player is exiting
function PlayerExitingGame(Controller Player);

function PlayerJoiningGame(out string Portal, out string Options);

function PlayerJoinedGame(Controller Player/*, optional bool bInvalidId*/);

// N.B. When bNeedDelayedJoinedEvent == True 'PlayerSpawning' is called BEFORE this when a player joins
function DelayedJoinedGame(Controller Player, optional bool bInvalidId);

function NotifySeamlessTravel(bool bToEntry, out array<Actor> ActorList)
{
	ActorList[ActorList.Length] = Self;

	// ResetBalancing gives you the chance to unload any per-level objects, if necessary
	if (bToEntry)
		ResetBalancing();
}


// 'TTeamFix' events (called from this class)

// This particular 'event' is nearly useless, I've only added it so that the switch-announcement code can be implemented in a subclass
function NotifySwitchPlayers(array<PlayerController> Players);


// ===== General functions

// Not needed anymore since TTF must be loaded through mutator now
/*
function PostBeginPlay()
{
	if (UTTeamGame(WorldInfo.Game) != none)
		InitializeBalancing();
}
*/

function InitializeBalancing()
{
	local Mutator m;
	local GameRules gr;

	// Load the assigned config profile and setup the property defaults
	LoadPresetConfig();
	InitializeDefaults();


	// NOTE: Not always reliable, this function also gets called from InitGame->InitMutator, which is before the GRI is spawned
	GRI = WorldInfo.Game.GameReplicationInfo;


	// Make sure the EventMutator class is not already in the mutator list
	if (EventMutator == none)
		for (m=WorldInfo.Game.BaseMutator; m!=none; m=m.NextMutator)
			if (TTeamFixMut(m) != none)
				EventMutator = TTeamFixMut(m);

	// Same with rules list
	if (EventRules == none)
		for (gr=WorldInfo.Game.GameRulesModifiers; gr!=none; gr=gr.NextGameRules)
			if (TTeamFixRules(gr) != none)
				EventRules = TTeamFixRules(gr);


	// Initiate the mutator and gamerules objects (mutator is always required now, in order to get level switch events etc.)
	/*
	if (EventMutator == none)
	{
		EventMutator = Spawn(EventMutClass);
		EventMutator.bUserAdded = True;

		if (WorldInfo.Game.BaseMutator == none)
			WorldInfo.Game.BaseMutator = EventMutator;
		else
			WorldInfo.Game.BaseMutator.AddMutator(EventMutator);
	}
	*/

	EventMutator.InitializeEvents(Self);


	if (NeedRules())
	{
		if (EventRules == none)
		{
			EventRules = Spawn(EventRulesClass);

			if (WorldInfo.Game.GameRulesModifiers == none)
				WorldInfo.Game.GameRulesModifiers = EventRules;
			else
				WorldInfo.Game.GameRulesModifiers.AddGameRules(EventRules);
		}

		EventRules.InitializeEvents(Self);
	}
}

// Implement in subclasses
function ResetBalancing();
function InitializeDefaults();


// Configuration profile loading
function LoadPresetConfig()
{
	local string ConfigName, SuffixConfigName, FinalConfig;
	local array<string> KnownProfiles;
	local int sLen, cLen, i;

	if (ConfigProfileClass == none)
		return;

	// Make sure the config loader has an entry in UTGame.ini, so that admins can change config profiles
	Class'TTeamFixConfigLoader'.static.StaticSaveConfig();

	// Use the TTeamFixConfigLoader class (which stores config info in UTGame.ini) to find the desired config set
	ConfigName = Class'TTeamFixConfigLoader'.default.ActiveConfigurationProfile;

	if (Class'TTeamFixConfigLoader'.default.bSuffixGameToProfile)
	{
		SuffixConfigName = string(WorldInfo.Game.Class);
		SuffixConfigName = ConfigName$"_"$SuffixConfigName;//Mid(SuffixConfigName, InStr(SuffixConfigName, ".") + 1);
	}


	// Check that the config profile exists (n.b. output values will be in the format "ConfigName ConfigProfileClass")
	if (GetPerObjectConfigSections(ConfigProfileClass, KnownProfiles))
	{
		// The code doesn't appear to like string@string within condition statements
		SuffixConfigName @= "";
		ConfigName @= "";
		sLen = Len(SuffixConfigName);
		cLen = Len(ConfigName);

		for (i=0; i<KnownProfiles.Length; ++i)
		{
			// If the prefixed profile exists, it has precedence over the non-prefixed profile...store it and break
			if (SuffixConfigName != "" && (Left(KnownProfiles[i], sLen) ~= SuffixConfigName))
			{
				FinalConfig = Left(SuffixConfigName, sLen-1);
				break;
			}

			if (Left(KnownProfiles[i], cLen) ~= ConfigName)
				FinalConfig = Left(ConfigName, cLen-1);
		}

		SuffixConfigName = Left(SuffixConfigName, sLen-1);
		ConfigName = Left(ConfigName, cLen-1);
	}

	// Log success/failure in finding configuration profiles
	if (SuffixConfigName != "" && FinalConfig != SuffixConfigName)
		LogInternal("Could not find configuration profile '"$SuffixConfigName$"' for loading", 'TitanTeamFix');
	else if (ConfigName != "" && FinalConfig != ConfigName && FinalConfig != SuffixConfigName)
		LogInternal("Could not find configuration profile '"$ConfigName$"' for loading", 'TitanTeamFix');


	// Assign the found configuration, defaulting if not found
	if (FinalConfig == "")
		ConfigName = "default";
	else
		ConfigName = FinalConfig;


	LogInternal("Loading configuration profile '"$ConfigName$"'", 'TitanTeamFix');


	// Load the object which contains the specified config information
	ConfigObject = new(none, ConfigName) ConfigProfileClass;

	// If the default config profile was loaded, make sure that profile exists (otherwise, create it)
	if (ConfigName ~= "default")
	{
		for (i=0; i<KnownProfiles.Length-1; ++i)
			if (Left(KnownProfiles[i], 8) ~= "default ")
				break;

		if (i >= KnownProfiles.Length || !(Left(KnownProfiles[i], 8) ~= "default "))
		{
			LogInternal("Default profile did not exist, creating default profile", 'TitanTeamFix');

			ConfigObject.SaveConfig();
		}
	}

	// Transfer the properties
	ConfigObject.TransferProperties(Self);
}


// ===== Checking functions

// This returns true if the current configuration needs events from the gamerules class
function bool NeedRules()
{
	if (bNeedPreSpawnEvent || bNeedKilledEvent)
		return true;

	return false;
}

function bool bTeamsUneven(optional out int BiggerTeam, optional out int Imbalance, optional bool bForceCheck)
{
	local int TeamCount[2];
	local PlayerController PC;


	if (GRI == none)
	{
		GRI = WorldInfo.Game.GameReplicationInfo;

		if (GRI == none)
			return false;
	}


	if (!bForceCheck && WorldInfo.Game.bGameEnded)
		return false;


	foreach WorldInfo.AllControllers(Class'PlayerController', PC)
		if (ExitingPlayers.Find(PC) == -1 && PC.PlayerReplicationInfo != none && !PC.PlayerReplicationInfo.bOnlySpectator)
			++TeamCount[PC.GetTeamNum()];


	Imbalance = Abs(TeamCount[0] - TeamCount[1]);
	BiggerTeam = int(TeamCount[1] > TeamCount[0]);

	// The teams are only considered to be uneven if a team has 2 or more extra players
	if (Imbalance >= 2)
		return True;

	return false;
}

/*
// Normally this function is disabled during endgame, use bForceCheck to bypass this if you need to
function bool bTeamsUneven(optional bool bForceCheck)
{
	if (GRI == none)
	{
		GRI = WorldInfo.Game.GameReplicationInfo;

		if (GRI == none)
			return false;
	}

	if (!bForceCheck && WorldInfo.Game.bGameEnded)
		return false;

	return (GRI.Teams[0] == none || GRI.Teams[1] == none || Abs(GRI.Teams[0].Size - GRI.Teams[1].Size) >= 2);
}

// Returns the overall bigger team
function int GetImbalance(optional out int BiggerHumanTeam, optional out int HumanImbalance, optional out int BiggerBotTeam, optional out int BotImbalance)
{
	local Controller C;
	local int HumanCount[2], BotCount[2];

	foreach WorldInfo.AllControllers(Class'Controller', C)
	{
		if (ExitingPlayers.Find(C) == -1 && C.PlayerReplicationInfo != none && !C.PlayerReplicationInfo.bOnlySpectator)
		{
			if (PlayerController(C) != none)
				++HumanCount[C.GetTeamNum()];
			else if (AIController(C) != none)
				++BotCount[C.GetTeamNum()];
		}
	}

	HumanImbalance = Abs(HumanCount[0] - HumanCount[1]);
	BotImbalance = Abs(BotCount[0] - BotCount[1]);

	BiggerHumanTeam = int(HumanCount[1] > HumanCount[0]);
	BiggerBotTeam = int(BotCount[1] > BotCount[0]);

	return int((HumanCount[1] + BotCount[1]) > (HumanCount[0] + BotCount[0]));
}
*/

final function bool bRecentlyJoined(Controller C)
{
	if (GRI == none)
	{
		GRI = WorldInfo.Game.GameReplicationInfo;

		if (GRI == none)
			return True;
	}

	return (GRI.ElapsedTime - C.PlayerReplicationInfo.StartTime <= RecentCutoffTime);
}

// Returns true if all the players on either team are dead
// TODO: Perhaps modify the 'PlayerKilled' event so that you don't have to specify the 'NewDeath' parameter when calling this from that function
function bool CheckTeamDeath(optional Controller NewDeath)
{
	local Controller C;
	local byte TeamIsAlive[2];

	bTestThisCode = True;
	// This function has never been tested

	foreach WorldInfo.AllControllers(Class'Controller', C)
	{
		if (C == NewDeath || ExitingPlayers.Find(C) != -1)
			continue;

		if (!C.IsDead() && !C.PlayerReplicationInfo.bOutOfLives)
			TeamIsAlive[C.GetTeamNum()] = 1;
	}

	return bool(TeamIsAlive[0]) || bool(TeamIsAlive[1]);
}

function EvenTeams(int BiggerTeam, int Imbalance)
{
	local PlayerController PC;
	local int i, j;
	local array<PlayerController> SwitchCandidates;
	local SwitchList S;


	foreach WorldInfo.AllControllers(Class'PlayerController', PC)
		if (PC.GetTeamNum() == BiggerTeam && ExitingPlayers.Find(PC) == -1 && PC.PlayerReplicationInfo != none && !PC.PlayerReplicationInfo.bOnlySpectator)
			SwitchCandidates.AddItem(PC);


	// Now sift through the 'SwitchHistory' list for players on the bigger team, and move them to the bottom of the SwitchCandidates list
	// N.B. 'SwitchHistory' is automatically sorted based upon 'SwitchCount'
	foreach SwitchHistory(S)
	{
		if (S.Player != none && S.Player.GetTeamNum() == BiggerTeam)
		{
			SwitchCandidates.RemoveItem(S.Player);
			SwitchCandidates.AddItem(S.Player);
		}
	}


	// Now that the list has been generated and sorted, determine the number of players which need to be switched and switch them
	j = (Imbalance & 0xFFFE) / 2; // 0xFFFE strips out odd values...3 becomes 2, 5 become 4 etc.


	SwitchCandidates.Length = j;

	NotifySwitchPlayers(SwitchCandidates);

	for (i=0; i<SwitchCandidates.Length; ++i)
		SwitchPlayer(SwitchCandidates[i]);


	// If the teams are still imbalanced by raw number, then rebalance the bots
	// TODO
}

function SwitchPlayer(PlayerController PC)
{
	local int i, NewPos;
	local SwitchList NewEntry, SH;


	// Check if the current controller is in the switch history list (cleaning the list as you go).
	/*
	CurEntry = -1;
	NewPos = SwitchHistory.Length;

	for (i=0; i<SwitchHistory.Length; ++i)
	{
		// Remove null entries (making sure to reduce NewPos as well)
		if (SwitchHistory[i].Player == none)
		{
			SwitchHistory.Remove(i, 1);
			--NewPos;
			--i;

			continue;
		}


		if (CurEntry == -1)
		{
			if (SwitchHistory[i].Player == C)
				CurEntry = i;
		}
		// If the current controller has been found in the list, try to find a place to reposition him whilst the list continues to iterate
		else if (NewPos == SwitchHistory.Length)
		{
			// ***
			bRemoveDebugCode = True;
			//LogInternal("TTeamFix::SwitchPlayer: Searching for suitable repositioning spot in SwitchHistory", 'TTFDebug');

			if (bDebugLogging)
				DebugLog.Logf("TTFDebug: TTeamFix::SwitchPlayer: Searching for suitable repositioning spot in SwitchHistory");
			// ***

			// Order by SwitchCount first, and then by StartTime
			if (SwitchHistory[CurEntry].SwitchCount + 1 > SwitchHistory[i].SwitchCount
				|| C.PlayerReplicationInfo.StartTime >= SwitchHistory[i].Player.PlayerReplicationInfo.StartTime)
			{
				// ***
				bRemoveDebugCode = True;

				/*
				LogInternal("TTeamFix::SwitchPlayer: Determined new repositioning spot; compared variables were:", 'TTFDebug');
				LogInternal("---", 'TTFDebug');
				LogInternal("SwitchCount:"@(SwitchHistory[CurEntry].SwitchCount + 1)@">"@SwitchHistory[i].SwitchCount, 'TTFDebug');
				LogInternal("StartTime:"@C.PlayerReplicationInfo.StartTime@">="@SwitchHistory[i].Player.PlayerReplicationInfo.StartTime, 'TTFDebug');
				LogInternal("---", 'TTFDebug');
				*/

				if (bDebugLogging)
				{
					DebugLog.Logf("TTFDebug: TTeamFix::SwitchPlayer: Determined new repositioning spot; compared variables were:");
					DebugLog.Logf("TTFDebug: ---");
					DebugLog.Logf("TTFDebug: SwitchCount:"@(SwitchHistory[CurEntry].SwitchCount + 1)@">"@SwitchHistory[i].SwitchCount);
					DebugLog.Logf("TTFDebug: StartTime:"@C.PlayerReplicationInfo.StartTime@">="@SwitchHistory[i].Player.PlayerReplicationInfo.StartTime);
					DebugLog.Logf("TTFDebug: ---");
				}
				// ***

				NewPos = i;
			}
		}
	}

	NewEntry.Player = C;
	NewEntry.SwitchCount = (CurEntry == -1) ? 1 : SwitchHistory[CurEntry].SwitchCount;
	*/


	// There is too much potential for bugs in the above code; this is less efficient but keeps things simple (perhaps recode later)


	// First clean the 'SwitchHistory' list
	i = SwitchHistory.Find('Player', None);

	while (i != -1)
	{
		SwitchHistory.Remove(i, 1);
		i = SwitchHistory.Find('Player', None);
	}


	// Now check if the current controller is already in the list (removing if found) and setup the new entry data
	i = SwitchHistory.Find('Player', PC);

	NewEntry.Player = PC;

	if (i == -1)
	{
		NewEntry.SwitchCount = 1;
	}
	else
	{
		NewEntry.SwitchCount = SwitchHistory[i].SwitchCount + 1;
		SwitchHistory.Remove(i, 1);
	}


	// Find a suitable location for the controller within the list
	NewPos = -1;

	foreach SwitchHistory(SH, i)
	{
		// Sorted first by SwitchCount and then StartTime
		if (NewEntry.SwitchCount < SH.SwitchCount ||
			(NewEntry.SwitchCount == SH.SwitchCount && PC.PlayerReplicationInfo.StartTime >= SH.Player.PlayerReplicationInfo.StartTime))
		{
			NewPos = i;
			break;
		}
	}

	// If a suitable location was not found, add to the end of the list, otherwise insert directly
	if (NewPos == -1)
	{
		NewPos = SwitchHistory.Length;
		SwitchHistory.Length = NewPos + 1;

		SwitchHistory[NewPos] = NewEntry;
	}
	else
	{
		SwitchHistory.InsertItem(NewPos, NewEntry);
	}



	// Switch the player
	TTFChangeTeam(PC, int(!bool(PC.GetTeamNum())));
}


// ===== Cannibalized versions of team-changing functions defined in gameinfo and its subclasses

function TTFChangeTeam(Controller Other, int num)
{
	local bool bOldKillEvent;
	local PlayerController PC;
	local UTOnslaughtGame OG;
	local UTOnslaughtPRI PRI;
	local UTPlayerReplicationInfo DefPRI;
	local int i, OldScore, OldDeaths, OldLives, OldSpree;
	local bool bOldOutOfLives;

	// This deprecated debug msg is here to remind me to check the latest patch code to see if any of this ripped code has changed
	bCheckAgainstPatch_LastVer_Patch1 = True;


	// Very important: You need to temporarily disable kill events, otherwise PlayerKilled could attempt to balance teams while this is calling
	bOldKillEvent = EventRules.bKillEvent;
	EventRules.bKillEvent = False;



	TTFSetTeam(Other, UTTeamGame(WorldInfo.Game).Teams[num]);


	OG = UTOnslaughtGame(WorldInfo.Game);

	if (OG != none)
	{
		foreach LocalPlayerControllers(Class'PlayerController', PC)
		{
			if (Other == PC)
				for (i=0; i<OG.PowerNodes.Length; ++i)
					OG.PowerNodes[i].UpdateEffects(False);

			break;
		}


		PRI = UTOnslaughtPRI(Other.PlayerReplicationInfo);

		if (PRI != none)
		{
			PRI.StartObjective = None;
			PRI.TemporaryStartObjective = None;
		}
	}


	if (Other.Pawn != none)
	{
		DefPRI = UTPlayerReplicationInfo(Other.PlayerReplicationInfo);

		OldScore =		DefPRI.Score;
		OldDeaths =		DefPRI.Deaths;
		OldLives =		DefPRI.NumLives;	// dunno if this might cause bugs...but it's no worse than leaving it out altogether
		bOldOutOfLives =	DefPRI.bOutOfLives;	// ^^
		OldSpree =		DefPRI.Spree;

		Other.Pawn.PlayerChangedTeam();

		DefPRI.Score =		OldScore;
		DefPRI.Deaths =		OldDeaths;
		DefPRI.NumLives =	OldLives;
		DefPRI.bOutOfLives =	bOldOutOfLives;
		DefPRI.Spree =		OldSpree;	
	}


	// Reset kill events now that the player has been switched (N.B. Keep this at the bottom of the function)
	EventRules.bKillEvent = bOldKillEvent;
}

function TTFSetTeam(Controller Other, UTTeamInfo NewTeam)
{
	local UTPlayerReplicationInfo PRI;
	local actor A;

	// This deprecated debug msg is here to remind me to check the latest patch code to see if any of this ripped code has changed
	bCheckAgainstPatch_LastVer_Patch1 = True;


	if (Other.PlayerReplicationInfo == none)
		return;

	if (Other.PlayerReplicationInfo.Team != none || !WorldInfo.Game.ShouldSpawnAtStartSpot(Other))
		Other.StartSpot = None;


	if (WorldInfo.NetMode != NM_DedicatedServer)
	{
		PRI = UTPlayerReplicationInfo(Other.PlayerReplicationInfo);

		if (PRI != none && !PRI.IsLocalPlayerPRI())
			PRI.SetCharacterMesh(None);
	}

	if (Other.PlayerReplicationInfo.Team != none)
	{
		Other.PlayerReplicationInfo.Team.RemoveFromTeam(Other);
		Other.PlayerReplicationInfo.Team = none;
	}

	if (NewTeam != none)
		NewTeam.AddToTeam(Other);


	if (PlayerController(Other) != none && LocalPlayer(PlayerController(Other).Player) != none)
		foreach AllActors(Class'Actor', A)
			A.NotifyLocalPlayerTeamReceived();

	if (WorldInfo.NetMode != NM_DedicatedServer && PRI != none && UTGameReplicationInfo(WorldInfo.GRI) != none)
		UTGameReplicationInfo(WorldInfo.GRI).ProcessCharacterData(PRI, True);
}


defaultproperties
{
	EventMutClass=Class'TTeamFixMut'
	EventRulesClass=Class'TTeamFixRules'

	RecentCutoffTime=120
}