//========= Copyright Valve Corporation, All rights reserved. ============//
//----------------------------------------------------------------------------------------------------------------
// tf_bot_manager.cpp
// Team Fortress NextBotManager
// Tom Bui, February 2010
//----------------------------------------------------------------------------------------------------------------

#include "cbase.h"
#include "tf_bot_manager.h"

#include "Player/NextBotPlayer.h"
#include "team.h"
#include "tf_bot.h"
#include "tf_gamerules.h"
#include "bot/map_entities/tf_bot_hint.h"
#include "bot/map_entities/tf_bot_hint_sentrygun.h"
#include "bot/map_entities/tf_bot_hint_teleporter_exit.h"


//----------------------------------------------------------------------------------------------------------------

// Creates and sets CTFBotManager as the NextBotManager singleton
static CTFBotManager sTFBotManager;

extern ConVar tf_bot_force_class;
ConVar tf_bot_difficulty( "tf_bot_difficulty", "1", FCVAR_NONE, "Defines the skill of bots joining the game.  Values are: 0=easy, 1=normal, 2=hard, 3=expert." );
ConVar tf_bot_quota( "tf_bot_quota", "0", FCVAR_NONE, "Determines the total number of tf bots in the game." );
ConVar tf_bot_quota_mode( "tf_bot_quota_mode", "normal", FCVAR_NONE, "Determines the type of quota.\nAllowed values: 'normal', 'fill', and 'match'.\nIf 'fill', the server will adjust bots to keep N players in the game, where N is bot_quota.\nIf 'match', the server will maintain a 1:N ratio of humans to bots, where N is bot_quota." );
ConVar tf_bot_join_after_player( "tf_bot_join_after_player", "1", FCVAR_NONE, "If nonzero, bots wait until a player joins before entering the game." );
ConVar tf_bot_auto_vacate( "tf_bot_auto_vacate", "1", FCVAR_NONE, "If nonzero, bots will automatically leave to make room for human players." );
ConVar tf_bot_offline_practice( "tf_bot_offline_practice", "0", FCVAR_NONE, "Tells the server that it is in offline practice mode." );
ConVar tf_bot_melee_only( "tf_bot_melee_only", "0", FCVAR_GAMEDLL, "If nonzero, TFBots will only use melee weapons" );

extern const char *GetRandomBotName( void );
extern void CreateBotName( int iTeam, int iClassIndex, CTFBot::DifficultyType skill, char* pBuffer, int iBufferSize );

static bool UTIL_KickBotFromTeam( int kickTeam )
{
	int i;

	// try to kick a dead bot first
	for ( i = 1; i <= gpGlobals->maxClients; ++i )
	{
		CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) );
		CTFBot* pBot = dynamic_cast<CTFBot*>(pPlayer);

		if (pBot == NULL)
			continue;

		if ( pBot->HasAttribute( CTFBot::QUOTA_MANANGED ) == false )
			continue;

		if ( ( pPlayer->GetFlags() & FL_FAKECLIENT ) == 0 )
			continue;

		if ( !pPlayer->IsAlive() && pPlayer->GetTeamNumber() == kickTeam )
		{
			// its a bot on the right team - kick it
			engine->ServerCommand( UTIL_VarArgs( "kickid %d\n", pPlayer->GetUserID() ) );

			return true;
		}
	}

	// no dead bots, kick any bot on the given team
	for ( i = 1; i <= gpGlobals->maxClients; ++i )
	{
		CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) );
		CTFBot* pBot = dynamic_cast<CTFBot*>(pPlayer);

		if (pBot == NULL)
			continue;

		if ( pBot->HasAttribute( CTFBot::QUOTA_MANANGED ) == false )
			continue;

		if ( ( pPlayer->GetFlags() & FL_FAKECLIENT ) == 0 )
			continue;

		if (pPlayer->GetTeamNumber() == kickTeam)
		{
			// its a bot on the right team - kick it
			engine->ServerCommand( UTIL_VarArgs( "kickid %d\n", pPlayer->GetUserID() ) );

			return true;
		}
	}

	return false;
}

//----------------------------------------------------------------------------------------------------------------

CTFBotManager::CTFBotManager()
	: NextBotManager()
	, m_flNextPeriodicThink( 0 )
{
	NextBotManager::SetInstance( this );
}


//----------------------------------------------------------------------------------------------------------------
CTFBotManager::~CTFBotManager()
{
	NextBotManager::SetInstance( NULL );
}


//----------------------------------------------------------------------------------------------------------------
void CTFBotManager::OnMapLoaded( void )
{
	NextBotManager::OnMapLoaded();

	ClearStuckBotData();
}


//----------------------------------------------------------------------------------------------------------------
void CTFBotManager::OnRoundRestart( void )
{
	NextBotManager::OnRoundRestart();

	// clear all hint ownership
	CTFBotHint *hint = NULL;
	while( ( hint = (CTFBotHint *)( gEntList.FindEntityByClassname( hint, "func_tfbot_hint" ) ) ) != NULL )
	{
		hint->SetOwnerEntity( NULL );
	}

	CTFBotHintSentrygun *sentryHint = NULL;
	while( ( sentryHint = (CTFBotHintSentrygun *)( gEntList.FindEntityByClassname( sentryHint, "bot_hint_sentrygun" ) ) ) != NULL )
	{
		sentryHint->SetOwnerEntity( NULL );
	}

	CTFBotHintTeleporterExit *teleporterHint = NULL;
	while( ( teleporterHint = (CTFBotHintTeleporterExit *)( gEntList.FindEntityByClassname( teleporterHint, "bot_hint_teleporter_exit" ) ) ) != NULL )
	{
		teleporterHint->SetOwnerEntity( NULL );
	}


#ifdef TF_CREEP_MODE
	m_creepExperience[ TF_TEAM_RED ] = 0;
	m_creepExperience[ TF_TEAM_BLUE ] = 0;
#endif

	m_isMedeivalBossScenarioSetup = false;
}


//----------------------------------------------------------------------------------------------------------------
void CTFBotManager::Update()
{
	MaintainBotQuota();

	DrawStuckBotData();

#ifdef TF_CREEP_MODE
	UpdateCreepWaves();
#endif

	NextBotManager::Update();
}


#ifdef TF_CREEP_MODE
ConVar tf_creep_initial_delay( "tf_creep_initial_delay", "30" );
ConVar tf_creep_wave_interval( "tf_creep_wave_interval", "30" );
ConVar tf_creep_wave_count( "tf_creep_wave_count", "3" );
ConVar tf_creep_class( "tf_creep_class", "heavyweapons" );
ConVar tf_creep_level_up( "tf_creep_level_up", "6" );


//----------------------------------------------------------------------------------------------------------------
void CTFBotManager::UpdateCreepWaves()
{
	if ( !TFGameRules()->IsCreepWaveMode() )
		return;

	if ( TFGameRules()->RoundHasBeenWon() )
	{
		// no more creep waves - game is over
		return;
	}

	if ( TFGameRules()->InSetup() || TFGameRules()->State_Get() == GR_STATE_STARTGAME || TFGameRules()->State_Get() == GR_STATE_PREROUND )
	{
		// no creeps at start of round
		m_creepWaveTimer.Start( tf_creep_initial_delay.GetFloat() );

		// delete all creeps
		for( int i=1; i<=gpGlobals->maxClients; ++i )
		{
			CBasePlayer *player = static_cast< CBasePlayer * >( UTIL_PlayerByIndex( i ) );

			if ( !player )
				continue;

			if ( FNullEnt( player->edict() ) )
				continue;

			CTFBot *creep = ToTFBot( player );
			if ( !creep || !creep->HasAttribute( CTFBot::IS_NPC ) )
				continue;

			engine->ServerCommand( UTIL_VarArgs( "kickid %d\n", player->GetUserID() ) );
		}

		return;
	}	

	if ( m_creepWaveTimer.IsElapsed() )
	{
		m_creepWaveTimer.Start( tf_creep_wave_interval.GetFloat() );

		SpawnCreepWave( TF_TEAM_RED );
		SpawnCreepWave( TF_TEAM_BLUE );
	}
}


//----------------------------------------------------------------------------------------------------------------
void CTFBotManager::SpawnCreepWave( int team )
{
	CTFBotSquad *squad = new CTFBotSquad;

	for( int i=0; i<tf_creep_wave_count.GetInt(); ++i )
	{
		SpawnCreep( team, squad );
	}
}


//----------------------------------------------------------------------------------------------------------------
void CTFBotManager::SpawnCreep( int team, CTFBotSquad *squad )
{
	CTFBot *bot = NextBotCreatePlayerBot< CTFBot >( "Creep" );

	if ( !bot ) 
		return;

	bot->SetAttribute( CTFBot::IS_NPC );
	bot->HandleCommand_JoinTeam( team == TF_TEAM_RED ? "red" : "blue" );
	bot->SetDifficulty( CTFBot::NORMAL );
	bot->HandleCommand_JoinClass( tf_creep_class.GetString() );
	bot->JoinSquad( squad );
	bot->AddGlowEffect();
	//BotGenerateAndWearItem( bot, "Honest Halo" );
}


//----------------------------------------------------------------------------------------------------------------
void CTFBotManager::OnCreepKilled( CTFPlayer *killer )
{
	CTFBot *bot = ToTFBot( killer );
	if ( bot && bot->HasAttribute( CTFBot::IS_NPC ) )
		return;

	++m_creepExperience[ killer->GetTeamNumber() ];

/*
	int xp = m_creepExperience[ killer->GetTeamNumber() ];
	int level = xp / tf_creep_level_up.GetInt();
	int left = xp % tf_creep_level_up.GetInt();

	char text[256];
	Q_snprintf( text, sizeof(text), "%s killed a creep. %s team LVL = %d+%d/%d\n", 
				killer->GetPlayerName(), 
				killer->GetTeamNumber() == TF_TEAM_RED ? "Red" : "Blue", 
				level+1, left, tf_creep_level_up.GetInt() );

	UTIL_ClientPrintAll( HUD_PRINTTALK, text );
*/

	UTIL_ClientPrintAll( HUD_PRINTTALK, "%s killed a creep" );
}

#endif // TF_CREEP_MODE

//----------------------------------------------------------------------------------------------------------------
bool CTFBotManager::RemoveBotFromTeamAndKick( int nTeam )
{
	CUtlVector< CTFPlayer* > vecCandidates;

	// Gather potential candidates
	for ( int i = 1; i <= gpGlobals->maxClients; ++i )
	{
		CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) );

		if ( pPlayer == NULL )
			continue;

		if ( FNullEnt( pPlayer->edict() ) )
			continue;

		if ( !pPlayer->IsConnected() )
			continue;

		CTFBot* pBot = dynamic_cast<CTFBot*>( pPlayer );
		if ( pBot && pBot->HasAttribute( CTFBot::QUOTA_MANANGED ) )
		{
			if ( pBot->GetTeamNumber() == nTeam )
			{
				vecCandidates.AddToTail( pPlayer );
			}
		}
	}
	
	CTFPlayer *pVictim = NULL;
	if ( vecCandidates.Count() > 0 )
	{
		// first look for bots that are currently dead
		FOR_EACH_VEC( vecCandidates, i )
		{
			CTFPlayer *pPlayer = vecCandidates[i];
			if ( pPlayer && !pPlayer->IsAlive() )
			{
				pVictim = pPlayer;
				break;
			}
		}

		// if we didn't fine one, try to kick anyone on the team
		if ( !pVictim )
		{
			FOR_EACH_VEC( vecCandidates, i )
			{
				CTFPlayer *pPlayer = vecCandidates[i];
				if ( pPlayer )
				{
					pVictim = pPlayer;
					break;
				}
			}
		}
	}

	if ( pVictim )
	{
		if ( pVictim->IsAlive() )
		{
			pVictim->CommitSuicide();
		}
		pVictim->ForceChangeTeam( TEAM_UNASSIGNED ); // skipping TEAM_SPECTATOR because some servers don't allow spectators
		UTIL_KickBotFromTeam( TEAM_UNASSIGNED );
		return true;
	}

	return false;
}

//----------------------------------------------------------------------------------------------------------------
void CTFBotManager::MaintainBotQuota()
{
	if ( TheNavMesh->IsGenerating() )
		return;

	if ( g_fGameOver )
		return;

	// new players can't spawn immediately after the round has been going for some time
	if ( !TFGameRules() )
		return;

	// training mode controls the bots
	if ( TFGameRules()->IsInTraining() )
		return;

	// if it is not time to do anything...
	if ( gpGlobals->curtime < m_flNextPeriodicThink )
		return;

	// think every quarter second
	m_flNextPeriodicThink = gpGlobals->curtime + 0.25f;

	// don't add bots until local player has been registered, to make sure he's player ID #1
	if ( !engine->IsDedicatedServer() )
	{
		CBasePlayer *pPlayer = UTIL_GetListenServerHost();
		if ( !pPlayer )
			return;
	}

	// We want to balance based on who's playing on game teams not necessary who's on team spectator, etc.
	int nConnectedClients = 0;
	int nTFBots = 0;
	int nTFBotsOnGameTeams = 0;
	int nNonTFBotsOnGameTeams = 0;
	int nSpectators = 0;
	for ( int i = 1; i <= gpGlobals->maxClients; ++i )
	{
		CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) );

		if ( pPlayer == NULL )
			continue;

		if ( FNullEnt( pPlayer->edict() ) )
			continue;

		if ( !pPlayer->IsConnected() )
			continue;

		CTFBot* pBot = dynamic_cast<CTFBot*>( pPlayer );
		if ( pBot && pBot->HasAttribute( CTFBot::QUOTA_MANANGED ) )
		{
			nTFBots++;
			if ( pPlayer->GetTeamNumber() == TF_TEAM_RED || pPlayer->GetTeamNumber() == TF_TEAM_BLUE )
			{
				nTFBotsOnGameTeams++;
			}
		}
		else
		{
			if ( pPlayer->GetTeamNumber() == TF_TEAM_RED || pPlayer->GetTeamNumber() == TF_TEAM_BLUE )
			{
				nNonTFBotsOnGameTeams++;
			}
			else if ( pPlayer->GetTeamNumber() == TEAM_SPECTATOR )
			{
				nSpectators++;
			}
		}

		nConnectedClients++;
	}

	int desiredBotCount = tf_bot_quota.GetInt();
	int nTotalNonTFBots = nConnectedClients - nTFBots;

	if ( FStrEq( tf_bot_quota_mode.GetString(), "fill" ) )
	{
		desiredBotCount = MAX( 0, desiredBotCount - nNonTFBotsOnGameTeams );
	}
	else if ( FStrEq( tf_bot_quota_mode.GetString(), "match" ) )
	{
		// If bot_quota_mode is 'match', we want the number of bots to be bot_quota * total humans
		desiredBotCount = (int)MAX( 0, tf_bot_quota.GetFloat() * nNonTFBotsOnGameTeams );
	}

	// wait for a player to join, if necessary
	if ( tf_bot_join_after_player.GetBool() )
	{
		if ( ( nNonTFBotsOnGameTeams == 0 ) && ( nSpectators == 0 ) )
		{
			desiredBotCount = 0;
		}
	}

	// if bots will auto-vacate, we need to keep one slot open to allow players to join
	if ( tf_bot_auto_vacate.GetBool() )
	{
		desiredBotCount = MIN( desiredBotCount, gpGlobals->maxClients - nTotalNonTFBots - 1 );
	}
	else
	{
		desiredBotCount = MIN( desiredBotCount, gpGlobals->maxClients - nTotalNonTFBots );
	}

	// add bots if necessary
	if ( desiredBotCount > nTFBotsOnGameTeams )
	{
		// don't try to add a bot if it would unbalance
		if ( !TFGameRules()->WouldChangeUnbalanceTeams( TF_TEAM_BLUE, TEAM_UNASSIGNED ) ||
			 !TFGameRules()->WouldChangeUnbalanceTeams( TF_TEAM_RED, TEAM_UNASSIGNED ) )
		{
			CTFBot *pBot = GetAvailableBotFromPool();
			if ( pBot == NULL )
			{
				pBot = NextBotCreatePlayerBot< CTFBot >( GetRandomBotName() );
			}
			if ( pBot )
			{
				pBot->SetAttribute( CTFBot::QUOTA_MANANGED );

				// join a team before we pick our class, since we use our teammates to decide what class to be
				pBot->HandleCommand_JoinTeam( "auto" );

				const char *classname = FStrEq( tf_bot_force_class.GetString(), "" ) ? pBot->GetNextSpawnClassname() : tf_bot_force_class.GetString();
				pBot->HandleCommand_JoinClass( classname );

				// give the bot a proper name
				char name[256];
				CTFBot::DifficultyType skill = pBot->GetDifficulty();
				CreateBotName( pBot->GetTeamNumber(), pBot->GetPlayerClass()->GetClassIndex(), skill, name, sizeof( name ) );
				engine->SetFakeClientConVarValue( pBot->edict(), "name", name );
				
				// Keep track of any bots we add during a match
				CMatchInfo *pMatchInfo = GTFGCClientSystem()->GetMatch();
				if ( pMatchInfo )
				{
					pMatchInfo->m_nBotsAdded++;
				}
			}
		}
	}
	else if ( desiredBotCount < nTFBotsOnGameTeams )
	{
		// kick a bot to maintain quota
		
		// first remove any unassigned bots
		if ( UTIL_KickBotFromTeam( TEAM_UNASSIGNED ) )
			return;

		int kickTeam;

		CTeam *pRed = GetGlobalTeam( TF_TEAM_RED );
		CTeam *pBlue = GetGlobalTeam( TF_TEAM_BLUE );

		// remove from the team that has more players
		if ( pBlue->GetNumPlayers() > pRed->GetNumPlayers() )
		{
			kickTeam = TF_TEAM_BLUE;
		}
		else if ( pBlue->GetNumPlayers() < pRed->GetNumPlayers() )
		{
			kickTeam = TF_TEAM_RED;
		}
		// remove from the team that's winning
		else if ( pBlue->GetScore() > pRed->GetScore() )
		{
			kickTeam = TF_TEAM_BLUE;
		}
		else if ( pBlue->GetScore() < pRed->GetScore() )
		{
			kickTeam = TF_TEAM_RED;
		}
		else
		{
			// teams and scores are equal, pick a team at random
			kickTeam = (RandomInt( 0, 1 ) == 0) ? TF_TEAM_BLUE : TF_TEAM_RED;
		}

		// attempt to kick a bot from the given team
		if ( UTIL_KickBotFromTeam( kickTeam ) )
			return;

		// if there were no bots on the team, kick a bot from the other team
		UTIL_KickBotFromTeam( kickTeam == TF_TEAM_BLUE ? TF_TEAM_RED : TF_TEAM_BLUE );
	}
}


//----------------------------------------------------------------------------------------------------------------
bool CTFBotManager::IsAllBotTeam( int iTeam )
{
	CTeam *pTeam = GetGlobalTeam( iTeam );
	if ( pTeam == NULL )
	{
		return false;
	}

	// check to see if any players on the team are humans
	for ( int i = 0, n = pTeam->GetNumPlayers(); i < n; ++i )
	{
		CTFPlayer *pPlayer = ToTFPlayer( pTeam->GetPlayer( i ) );
		if ( pPlayer == NULL )
		{
			continue;
		}
		if ( pPlayer->IsBot() == false )
		{
			return false;
		}
	}

	// if we made it this far, then they must all be bots!
	if ( pTeam->GetNumPlayers() != 0 )
	{
		return true;
	}

	// okay, this is a bit trickier...
	// if there are no people on this team, then we need to check the "assigned" human team
	return TFGameRules()->GetAssignedHumanTeam() != iTeam;
}


//----------------------------------------------------------------------------------------------------------------
void CTFBotManager::SetIsInOfflinePractice(bool bIsInOfflinePractice)
{
	tf_bot_offline_practice.SetValue( bIsInOfflinePractice ? 1 : 0 );
}


//----------------------------------------------------------------------------------------------------------------
bool CTFBotManager::IsInOfflinePractice() const
{
	return tf_bot_offline_practice.GetInt() != 0;
}


//----------------------------------------------------------------------------------------------------------------
bool CTFBotManager::IsMeleeOnly() const
{
	return tf_bot_melee_only.GetBool();
}


//----------------------------------------------------------------------------------------------------------------
void CTFBotManager::RevertOfflinePracticeConvars()
{
	tf_bot_quota.Revert();
	tf_bot_quota_mode.Revert();
	tf_bot_auto_vacate.Revert();
	tf_bot_difficulty.Revert();
	tf_bot_offline_practice.Revert();
}


//----------------------------------------------------------------------------------------------------------------
void CTFBotManager::LevelShutdown()
{
	m_flNextPeriodicThink = 0.0f;
	if ( IsInOfflinePractice() )
	{
		RevertOfflinePracticeConvars();
		SetIsInOfflinePractice( false );
	}		
}


//----------------------------------------------------------------------------------------------------------------
CTFBot* CTFBotManager::GetAvailableBotFromPool()
{
	for ( int i = 1; i <= gpGlobals->maxClients; ++i )
	{
		CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) );
		CTFBot* pBot = dynamic_cast<CTFBot*>(pPlayer);

		if (pBot == NULL)
			continue;

		if ( ( pBot->GetFlags() & FL_FAKECLIENT ) == 0 )
			continue;

		if ( pBot->GetTeamNumber() == TEAM_SPECTATOR || pBot->GetTeamNumber() == TEAM_UNASSIGNED )
		{
			pBot->ClearAttribute( CTFBot::QUOTA_MANANGED );
			return pBot;
		}
	}
	return NULL;
}


//----------------------------------------------------------------------------------------------------------------
void CTFBotManager::OnForceAddedBots( int iNumAdded )
{
	tf_bot_quota.SetValue( tf_bot_quota.GetInt() + iNumAdded );
	m_flNextPeriodicThink = gpGlobals->curtime + 1.0f;
}


//----------------------------------------------------------------------------------------------------------------
void CTFBotManager::OnForceKickedBots( int iNumKicked )
{
	tf_bot_quota.SetValue( MAX( tf_bot_quota.GetInt() - iNumKicked, 0 ) );
	// allow time for the bots to be kicked
	m_flNextPeriodicThink = gpGlobals->curtime + 2.0f;
}


//----------------------------------------------------------------------------------------------------------------
CTFBotManager &TheTFBots( void )
{
	return static_cast<CTFBotManager&>( TheNextBots() );
}



//----------------------------------------------------------------------------------------------------------------
//----------------------------------------------------------------------------------------------------------------
CON_COMMAND_F( tf_bot_debug_stuck_log, "Given a server logfile, visually display bot stuck locations.", FCVAR_GAMEDLL | FCVAR_CHEAT )
{
	// Listenserver host or rcon access only!
	if ( !UTIL_IsCommandIssuedByServerAdmin() )
		return;

	if ( args.ArgC() < 2 )
	{
		DevMsg( "%s <logfilename>\n", args.Arg(0) );
		return;
	}

	FileHandle_t file = filesystem->Open( args.Arg(1), "r", "GAME" );

	const int maxBufferSize = 1024;
	char buffer[ maxBufferSize ];

	char logMapName[ maxBufferSize ];
	logMapName[0] = '\000';

	TheTFBots().ClearStuckBotData();

	if ( file )
	{
		int line = 0;
		while( !filesystem->EndOfFile( file ) )
		{
			filesystem->ReadLine( buffer, maxBufferSize, file );
			++line;

			strtok( buffer, ":" );
			strtok( NULL, ":" );
			strtok( NULL, ":" );
			char *first = strtok( NULL, " " );

			if ( !first )
				continue;

			if ( !strcmp( first, "Loading" ) )
			{
				// L 08/08/2012 - 15:10:47: Loading map "mvm_coaltown"
				strtok( NULL, " " );
				char *mapname = strtok( NULL, "\"" );

				if ( mapname )
				{
					strcpy( logMapName, mapname );
					Warning( "*** Log file from map '%s'\n", mapname );
				}
			}
			else if ( first[0] == '\"' )
			{
				// might be a player ID

				char *playerClassname = &first[1];

				char *nameEnd = playerClassname;
				while( *nameEnd != '\000' && *nameEnd != '<' )
					++nameEnd;
				*nameEnd = '\000';

				char *botIDString = ++nameEnd;
				char *IDEnd = botIDString;
				while( *IDEnd != '\000' && *IDEnd != '>' )
					++IDEnd;
				*IDEnd = '\000';

				int botID = atoi( botIDString );

				char *second = strtok( NULL, " " );
				if ( second && !strcmp( second, "stuck" ) )
				{
					CStuckBot *stuckBot = TheTFBots().FindOrCreateStuckBot( botID, playerClassname );

					CStuckBotEvent *stuckEvent = new CStuckBotEvent;


					// L 08/08/2012 - 15:15:05: "Scout<53><BOT><Blue>" stuck (position "-180.61 2471.29 216.04") (duration "2.52") L 08/08/2012 - 15:15:05:    path_goal ( "-180.61 2471.29 216.04" )
					strtok( NULL, " (\"" );	// (position

					stuckEvent->m_stuckSpot.x = (float)atof( strtok( NULL, " )\"" ) );
					stuckEvent->m_stuckSpot.y = (float)atof( strtok( NULL, " )\"" ) );
					stuckEvent->m_stuckSpot.z = (float)atof( strtok( NULL, " )\"" ) );

					strtok( NULL, ") (\"" );
					stuckEvent->m_stuckDuration = (float)atof( strtok( NULL, "\"" ) );

					strtok( NULL, ") (\"-L0123456789/:" );	// path_goal

					char *goal = strtok( NULL, ") (\"" );

					if ( goal && strcmp( goal, "NULL" ) )
					{
						stuckEvent->m_isGoalValid = true;

						stuckEvent->m_goalSpot.x = (float)atof( goal );
						stuckEvent->m_goalSpot.y = (float)atof( strtok( NULL, ") (\"" ) );
						stuckEvent->m_goalSpot.z = (float)atof( strtok( NULL, ") (\"" ) );
					}
					else
					{
						stuckEvent->m_isGoalValid = false;
					}

					stuckBot->m_stuckEventVector.AddToTail( stuckEvent );
				}
			}
		}

		filesystem->Close( file );
	}
	else
	{
		Warning( "Can't open file '%s'\n", args.Arg(1) );
	}

	//TheTFBots().DrawStuckBotData();
}


//----------------------------------------------------------------------------------------------------------------
//----------------------------------------------------------------------------------------------------------------
CON_COMMAND_F( tf_bot_debug_stuck_log_clear, "Clear currently loaded bot stuck data", FCVAR_GAMEDLL | FCVAR_CHEAT )
{
	// Listenserver host or rcon access only!
	if ( !UTIL_IsCommandIssuedByServerAdmin() )
		return;

	TheTFBots().ClearStuckBotData();
}


//----------------------------------------------------------------------------------------------------------------
// for parsing and debugging stuck bot server logs
void CTFBotManager::ClearStuckBotData()
{
	m_stuckBotVector.PurgeAndDeleteElements();
}


//----------------------------------------------------------------------------------------------------------------
// for parsing and debugging stuck bot server logs
CStuckBot *CTFBotManager::FindOrCreateStuckBot( int id, const char *playerClass )
{
	for( int i=0; i<m_stuckBotVector.Count(); ++i )
	{
		CStuckBot *stuckBot = m_stuckBotVector[i];

		if ( stuckBot->IsMatch( id, playerClass ) )
		{
			return stuckBot;
		}
	}

	// new instance of a stuck bot
	CStuckBot *newStuckBot = new CStuckBot( id, playerClass );
	m_stuckBotVector.AddToHead( newStuckBot );

	return newStuckBot;
}


//----------------------------------------------------------------------------------------------------------------
void CTFBotManager::DrawStuckBotData( float deltaT )
{
	if ( engine->IsDedicatedServer() )
		return;

	if ( !m_stuckDisplayTimer.IsElapsed() )
		return;

	m_stuckDisplayTimer.Start( deltaT );

	CBasePlayer *player = UTIL_GetListenServerHost();
	if ( player == NULL )
		return;

// 	Vector forward;
// 	AngleVectors( player->EyeAngles(), &forward );

	for( int i=0; i<m_stuckBotVector.Count(); ++i )
	{
		for( int j=0; j<m_stuckBotVector[i]->m_stuckEventVector.Count(); ++j )
		{
			m_stuckBotVector[i]->m_stuckEventVector[j]->Draw( deltaT );
		}

		for( int j=0; j<m_stuckBotVector[i]->m_stuckEventVector.Count()-1; ++j )
		{
			NDebugOverlay::HorzArrow( m_stuckBotVector[i]->m_stuckEventVector[j]->m_stuckSpot, 
									  m_stuckBotVector[i]->m_stuckEventVector[j+1]->m_stuckSpot,
									  3, 100, 0, 255, 255, true, deltaT );
		}

		NDebugOverlay::Text( m_stuckBotVector[i]->m_stuckEventVector[0]->m_stuckSpot, CFmtStr( "%s(#%d)", m_stuckBotVector[i]->m_name, m_stuckBotVector[i]->m_id ), false, deltaT );
	}
}