/*
   Copyright (C) 1997-2001 Id Software, Inc.

   This program is free software; you can redistribute it and/or
   modify it under the terms of the GNU General Public License
   as published by the Free Software Foundation; either version 2
   of the License, or (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

   See the GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

 */

#include "g_local.h"
#include "g_gametypes.h"


//==========================================================
//					Teams
//==========================================================

cvar_t *g_maxteams;
cvar_t *g_teams_maxplayers;
cvar_t *g_teams_teamdamage;
cvar_t *g_teams_allow_uneven;

//=================
//G_Teams_NewMap
//=================
void G_Teams_NewMap( void )
{
	int team;
	edict_t	*e;

	//unlock all teams and clear up team lists
	memset( teamlist, 0, sizeof( teamlist ) );
	for( team = TEAM_SPECTATOR; team < GS_MAX_TEAMS; team++ )
		teamlist[team].playerIndices[0] = -1;

	for( e = game.edicts + 1; PLAYERNUM( e ) < game.maxclients; e++ )
	{
		if( !e->r.inuse || !e->r.client || !e->r.client->pers.connected )
			continue;

		G_Teams_SetTeam( e, TEAM_SPECTATOR );
	}
}

//=================
//G_Teams_Init
//=================
void G_Teams_Init( void )
{
	//not much to do here, just start up the cvars
	g_maxteams = trap_Cvar_Get( "g_maxteams", "2", CVAR_ARCHIVE );
	if( g_maxteams->integer > GS_MAX_TEAMS - TEAM_PLAYERS )
	{
		trap_Cvar_Set( "g_maxteams", va( "%i", ( GS_MAX_TEAMS - TEAM_PLAYERS ) ) );
	}

	g_teams_maxplayers = trap_Cvar_Get( "g_teams_maxplayers", "0", CVAR_ARCHIVE );
	g_teams_teamdamage = trap_Cvar_Get( "g_teams_teamdamage", "1", CVAR_ARCHIVE );
	g_teams_allow_uneven = trap_Cvar_Get( "g_teams_allow_uneven", "1", CVAR_ARCHIVE );
}


//=================
//G_Teams_UpdateMembersList
// It's better to count the list in detail once per fame, than
// creating a quick list each time we need it.
//=================
void G_Teams_UpdateMembersList( void )
{
	static int list[MAX_CLIENTS];
	static int sorted[MAX_CLIENTS];
	static int count;

	edict_t	*ent;
	int i, team;
	int bestscore;
	int bestplayer = 0;

	for( team = TEAM_SPECTATOR; team < TEAM_ALPHA + g_maxteams->integer; team++ )
	{
		teamlist[team].numplayers = 0;
		teamlist[team].has_coach = qfalse;

		count = 0;
		//create a temp list with the clients inside this team
		for( i = 0, ent = game.edicts + 1; i < game.maxclients; i++, ent++ )
		{
			if( !ent->r.client || ( trap_GetClientState( PLAYERNUM( ent ) ) < CS_CONNECTED ) )
				continue;

			if( ent->s.team == team )
			{
				list[count] = ENTNUM( ent );
				count++;
				if( ent->r.client->is_coach )
					teamlist[team].has_coach = qtrue;
			}
		}

		if( count )
		{
			memset( sorted, 0, sizeof( int )*MAX_CLIENTS );
			bestplayer = -2;
			while( bestplayer != -1 )
			{
				bestscore = -9999;
				bestplayer = -1;
				//now sort them by their score
				for( i = 0; i < count; i++ )
				{
					if( !sorted[i] )
					{
						int score;
						ent = game.edicts + list[i];
						score = match.scores[PLAYERNUM( ent )].score;
						if( game.gametype == GAMETYPE_TDM )
							score = match.scores[PLAYERNUM( ent )].kills - match.scores[PLAYERNUM( ent )].teamfrags - match.scores[PLAYERNUM( ent )].deaths; // sort by Net
						if( game.gametype == GAMETYPE_CA )
							score = ent->r.client->resp.total_damage_given; // sort by Dmg

						if( score >= bestscore )
						{
							bestplayer = i;
							bestscore = score;
						}
					}
				}

				if( bestplayer > -1 )
				{
					sorted[bestplayer] = qtrue;
					teamlist[team].playerIndices[teamlist[team].numplayers] = list[bestplayer];
					teamlist[team].numplayers++;
				}
			}
		}

		//terminate the list with -1
		teamlist[team].playerIndices[teamlist[team].numplayers] = -1;
	}
}

//=================
//G_Teams_TeamIsLocked
//=================
qboolean G_Teams_TeamIsLocked( int team )
{
	if( team && team < GS_MAX_TEAMS )
		return teamlist[team].locked;
	else
		return qfalse;
}

//=================
//G_Teams_LockTeam
//=================
void G_Teams_LockTeam( int team )
{
	if( team && team < GS_MAX_TEAMS )
		teamlist[team].locked = qtrue;
}

//=================
//G_Teams_UnLockTeam
//=================
void G_Teams_UnLockTeam( int team )
{
	if( team && team < GS_MAX_TEAMS )
		teamlist[team].locked = qfalse;
}

//=================
//G_Teams_PlayerIsInvited
//=================
static qboolean G_Teams_PlayerIsInvited( int team, edict_t *ent )
{
	int i;

	if( team < TEAM_PLAYERS || team >= GS_MAX_TEAMS )
		return qfalse;

	if( !ent->r.inuse || !ent->r.client )
		return qfalse;

	for( i = 0; teamlist[team].invited[i] && i < MAX_CLIENTS; i++ )
	{
		if( teamlist[team].invited[i] == ENTNUM( ent ) )
			return qtrue;
	}

	return qfalse;
}

//=================
//G_Teams_InvitePlayer
//=================
static void G_Teams_InvitePlayer( int team, edict_t *ent )
{
	int i;

	if( team < TEAM_PLAYERS || team >= GS_MAX_TEAMS )
		return;

	if( !ent->r.inuse || !ent->r.client )
		return;

	for( i = 0; teamlist[team].invited[i] && i < MAX_CLIENTS; i++ )
	{
		if( teamlist[team].invited[i] == ENTNUM( ent ) )
			return;
	}

	teamlist[team].invited[i] = ENTNUM( ent );
}

//=================
//G_Teams_UnInvitePlayer
//=================
void G_Teams_UnInvitePlayer( int team, edict_t *ent )
{
	int i;

	if( team < TEAM_PLAYERS || team >= GS_MAX_TEAMS )
		return;

	if( !ent->r.inuse || !ent->r.client )
		return;

	for( i = 0; teamlist[team].invited[i] && i < MAX_CLIENTS; i++ )
	{
		if( teamlist[team].invited[i] == ENTNUM( ent ) )
			break;
	}
	while( teamlist[team].invited[i] && i+1 < MAX_CLIENTS )
	{
		teamlist[team].invited[i] = teamlist[team].invited[i+1];
		i++;
	}
	teamlist[team].invited[MAX_CLIENTS-1] = 0;
}

//=================
//G_Teams_RemoveInvites
// Removes all invites from all teams
//=================
void G_Teams_RemoveInvites( void )
{
	int team;

	for( team = TEAM_PLAYERS; team < GS_MAX_TEAMS; team++ )
		teamlist[team].invited[0] = 0;
}

//=================
//G_Teams_Invite_f
//=================
void G_Teams_Invite_f( edict_t *ent )
{
	char *text;
	edict_t *toinvite;
	int team;

	if( ( !ent->r.inuse || !ent->r.client ) )
		return;

	text = trap_Cmd_Argv( 1 );

	if( !text || !strlen( text ) )
	{
		int i;
		edict_t *e;
		char msg[1024];

		msg[0] = 0;
		Q_strncatz( msg, "Usage: invite <player>\n", sizeof( msg ) );
		Q_strncatz( msg, "- List of current players:\n", sizeof( msg ) );

		for( i = 0, e = game.edicts+1; i < game.maxclients; i++, e++ )
		{
			if( !e->r.inuse ) continue;
			Q_strncatz( msg, va( "%3i: %s\n", PLAYERNUM( e ), e->r.client->pers.netname ), sizeof( msg ) );
		}

		G_PrintMsg( ent, "%s", msg );
		return;
	}

	team = ent->s.team;

	if( !G_Teams_TeamIsLocked( team ) )
	{
		G_PrintMsg( ent, "Your team is not locked.\n" );
		return;
	}

	toinvite = G_PlayerForText( text );

	if( !toinvite )
	{
		G_PrintMsg( ent, "No such player.\n" );
		return;
	}

	if( G_Teams_PlayerIsInvited( team, toinvite ) )
	{
		G_PrintMsg( ent, "%s%s is already invited to your team.\n", toinvite->r.client->pers.netname, S_COLOR_WHITE );
		return;
	}

	G_Teams_InvitePlayer( team, toinvite );
	G_PrintMsg( NULL, "%s%s invited %s%s to team %s%s.\n", ent->r.client->pers.netname, S_COLOR_WHITE,
	            toinvite->r.client->pers.netname, S_COLOR_WHITE, GS_TeamName( team ), S_COLOR_WHITE );
}

//=================
//G_Teams_AssignTeamSkin
//=================
void G_Teams_AssignTeamSkin( edict_t *ent, char *userinfo )
{
	char skin[MAX_QPATH], model[MAX_QPATH];
	char *userskin, *usermodel;

	if( ent->s.team == TEAM_SPECTATOR )
	{
		ent->s.modelindex = ent->s.modelindex2 = 0;
		ent->s.skinnum = 0;
		return;
	}

	// index skin file
	userskin = GS_TeamSkinName( ent->s.team ); // is it a team skin?
	if( !userskin )
	{             // NULL indicates *user defined*
		userskin = Info_ValueForKey( userinfo, "skin" );
		if( !userskin || !userskin[0] || !COM_ValidateRelativeFilename( userskin ) || strchr( userskin, '/' ) )
			userskin = NULL;
	}

	// index player model
	usermodel = Info_ValueForKey( userinfo, "model" );
	if( !usermodel || !usermodel[0] || !COM_ValidateRelativeFilename( usermodel ) || strchr( usermodel, '/' ) )
		usermodel = NULL;

	if( userskin && usermodel )
	{
		Q_snprintfz( model, sizeof( model ), "$models/players/%s", usermodel );
		Q_snprintfz( skin, sizeof( skin ), "models/players/%s/%s", usermodel, userskin );
	}
	else
	{
		Q_snprintfz( model, sizeof( model ), "$models/players/%s", DEFAULT_PLAYERMODEL );
		Q_snprintfz( skin, sizeof( skin ), "models/players/%s/%s", DEFAULT_PLAYERMODEL, DEFAULT_PLAYERSKIN );
	}

	if( !ent->deadflag )
		ent->s.modelindex = trap_ModelIndex( model );
	ent->s.skinnum = trap_SkinIndex( skin );
}

//=================
//G_Teams_SetTeam - sets clients team without any checking
//=================
void G_Teams_SetTeam( edict_t *ent, int team )
{
	assert( ent && ent->r.inuse && ent->r.client );
	assert( team >= TEAM_SPECTATOR && team < GS_MAX_TEAMS );

	//clean scores at changing team
	memset( &match.scores[PLAYERNUM( ent )], 0, sizeof( client_scores_t ) );
	if( game.gametype == GAMETYPE_CTF )
	{
		G_Gametype_CTF_DeadDropFlag( ent );
		G_Gametype_CTF_CleanUpPlayerStats( ent );
	}
	// remove weapon in the case he had one
	ent->r.client->latched_weapon = WEAP_NONE;
	ChangeWeapon( ent );

	if( ent->s.team != team )
		ent->r.client->is_coach = qfalse;

	ent->s.team = ent->r.client->pers.team = team;

	if( team == TEAM_SPECTATOR )
	{
		client_persistant_t pers;
		client_teamchange_t teamchange;
		char userinfo[MAX_INFO_STRING];
		int i;

		// reset player ready state
		ent->s.teleported = qtrue;

		pers = ent->r.client->pers;
		teamchange = ent->r.client->teamchange;

		memcpy( userinfo, ent->r.client->pers.userinfo, sizeof( userinfo ) );
		memset( ent->r.client, 0, sizeof( *ent->r.client ) );
		ent->r.client->pers = pers;
		ent->r.client->teamchange = teamchange;
		ClientUserinfoChanged( ent, userinfo );

		// set the delta angle
		for( i = 0; i < 3; i++ )
			ent->r.client->ps.pmove.delta_angles[i] = ANGLE2SHORT( ent->s.angles[i] ) - ent->r.client->pers.cmd_angles[i];

		ent->deadflag = DEAD_NO;
		ent->movetype = MOVETYPE_NOCLIP;
		ent->r.solid = SOLID_NOT;
		ent->r.svflags |= SVF_NOCLIENT;
		ent->r.client->ps.pmove.pm_type = PM_SPECTATOR;
		ent->r.client->ps.pmove.pm_flags &= ~PMF_NO_PREDICTION;

		ent->s.modelindex = ent->s.modelindex2 = 0;
		ent->s.weapon = 0;

		GClip_LinkEntity( ent );

		G_ClearPlayerStateEvents( ent->r.client );

		// clear the entity snap info
		memset( &ent->snap, 0, sizeof( ent->snap ) );

	}
	else
	{    //known team found

		// call for spawn
		G_Teams_UnInvitePlayer( team, ent );
#ifdef DUEL_ARENA
		if( ( game.gametype == GAMETYPE_CA && match.state == MATCH_STATE_PLAYTIME ) ||
		   ( game.gametype == GAMETYPE_DA && match.state == MATCH_STATE_PLAYTIME ) )
		{
#else
		if( game.gametype == GAMETYPE_CA && match.state == MATCH_STATE_PLAYTIME )
		{
#endif
			// cannot respawn until the next round
			ent->health = 0;
			ent->movetype = MOVETYPE_NOCLIP;
			ent->r.solid = SOLID_NOT;
			ent->r.svflags |= SVF_NOCLIENT;
			ent->r.client->ps.pmove.pm_type = PM_SPECTATOR;
			ent->r.client->ps.pmove.pm_flags &= ~PMF_NO_PREDICTION;

			if( ent->ai.type != AI_ISBOT )
			{
				ent->r.client->chase.target = ENTNUM( ent );
				ent->r.client->chase.active = qtrue;
				ent->r.client->chase.teamonly = qtrue;
				G_ChasePlayer( ent, NULL, qtrue, 0 );
			}
		}
		else
		{
			if( !ent->r.client->is_coach )  // don't spawn coachs
				G_Gametype_ClientRespawn( ent ); // already updates the skin
		}
	}

	match.ready[PLAYERNUM( ent )] = qfalse;
	// reset ready up announcer timers
	ent->r.client->pers.readyUpWarningNext = game.realtime + 3000;
	ent->r.client->pers.readyUpWarningCount = 0;

	// reset spawncount etc.
	InitClientResp( ent->r.client );

	G_Teams_UpdateMembersList();
	G_Match_CheckReadys();
	G_UpdatePlayerMatchMsg( ent );
}

enum
{
	ER_TEAM_OK,
	ER_TEAM_INVALID,
	ER_TEAM_FULL,
	ER_TEAM_LOCKED,
	ER_TEAM_MATCHSTATE,
	ER_TEAM_CHALLENGERS,
	ER_TEAM_UNEVEN
};

static qboolean G_Teams_CanKeepEvenTeam( int team )
{
	int max = 0;
	int min = game.maxclients + 1;
	int i;

	for( i = TEAM_ALPHA; i < TEAM_ALPHA + g_maxteams->integer; i++ )
	{
		if( max < teamlist[i].numplayers )
			max = teamlist[i].numplayers;
		if( min > teamlist[i].numplayers )
			min = teamlist[i].numplayers;
	}

	return min == max ? qtrue : teamlist[team].numplayers != max;
}

//=================
//G_GameTypes_DenyJoinTeam
//=================
static int G_GameTypes_DenyJoinTeam( edict_t *ent, int team )
{
	if( team < 0 || team >= GS_MAX_TEAMS )
	{
		G_Printf( "WARNING: 'G_GameTypes_CanJoinTeam' parsing a unrecognized team value\n" );
		return ER_TEAM_INVALID;
	}

	if( team == TEAM_SPECTATOR )
		return ER_TEAM_OK;

	if( match.state > MATCH_STATE_PLAYTIME )
		return ER_TEAM_MATCHSTATE;

	// waiting for chanllengers queue to be executed
	if( G_Gametype_hasChallengersQueue( game.gametype ) &&
	   game.realtime < level.spawnedTimeStamp + (unsigned)( G_CHALLENGERS_MIN_JOINTEAM_MAPTIME + game.snapFrameTime ) )
		return ER_TEAM_CHALLENGERS;

	// force eveyone to go through queue so things work on map change
	if( G_Gametype_hasChallengersQueue( game.gametype ) && !ent->r.client->pers.queueTimeStamp )
		return ER_TEAM_CHALLENGERS;

	//see if team is locked
	if( G_Teams_TeamIsLocked( team ) && !G_Teams_PlayerIsInvited( team, ent ) )
	{
		return ER_TEAM_LOCKED;
	}

	if( GS_Gametype_IsTeamBased( game.gametype ) && team >= TEAM_ALPHA &&
	   team <= TEAM_DELTA && ( team - TEAM_ALPHA < g_maxteams->integer ) )
	{
		//see if team is full
		if( team > TEAM_PLAYERS )
		{
			int count = teamlist[team].numplayers;

			if( ( count + 1 > gametypes[game.gametype].maxPlayersPerTeam &&
			      gametypes[game.gametype].maxPlayersPerTeam > 0 ) ||
			   ( count + 1 > g_teams_maxplayers->integer &&
			     g_teams_maxplayers->integer > 0 ) )
			{
				return ER_TEAM_FULL;
			}
		}

		if( !g_teams_allow_uneven->integer && !G_Teams_CanKeepEvenTeam( team ) )
			return ER_TEAM_UNEVEN;

		return ER_TEAM_OK;
	}
	else if( team == TEAM_PLAYERS )
	{
		return ER_TEAM_OK;
	}

	return ER_TEAM_INVALID;
}

//=================
//G_Teams_JoinTeam - checks that client can join the given team and then joins it
//=================
qboolean G_Teams_JoinTeam( edict_t *ent, int team )
{
	int error;
	G_Teams_UpdateMembersList(); // make sure we have up-to-date data

	if( !ent->r.client )
		return qfalse;

	if( ( error = G_GameTypes_DenyJoinTeam( ent, team ) ) )
	{
		if( error == ER_TEAM_INVALID )
		{
			G_PrintMsg( ent, "Can't join %s in %s\n", GS_TeamName( team ),
			           GS_Gametype_ShortName( game.gametype ) );
		}
		else if( error == ER_TEAM_CHALLENGERS )
		{
			G_Teams_JoinChallengersQueue( ent );
		}
		else if( error == ER_TEAM_FULL )
		{
			G_PrintMsg( ent, "Team %s is FULL\n", GS_TeamName( team ) );
			G_Teams_JoinChallengersQueue( ent );
		}
		else if( error == ER_TEAM_LOCKED )
		{
			G_PrintMsg( ent, "Team %s is LOCKED\n", GS_TeamName( team ) );
			G_Teams_JoinChallengersQueue( ent );
		}
		else if( error == ER_TEAM_MATCHSTATE )
		{
			G_PrintMsg( ent, "Can't join %s at this moment\n", GS_TeamName( team ) );
		}
		else if( error == ER_TEAM_UNEVEN )
		{
			G_PrintMsg( ent, "Can't join %s because of uneven teams\n", GS_TeamName( team ) ); // FIXME: need more suitable message :P
			G_Teams_JoinChallengersQueue( ent );
		}
		return qfalse;
	}

	//ok, can join, proceed
	G_Teams_SetTeam( ent, team );

	// force client to popup CA menu.
#ifndef WSW_RELEASE
	if( game.gametype == GAMETYPE_CA && g_ca_classmode->integer && !g_instagib->integer && team != TEAM_SPECTATOR )
#else
	if( game.gametype == GAMETYPE_CA && !g_instagib->integer && team != TEAM_SPECTATOR )
#endif
		trap_GameCmd( ent, "mnca" );

	return qtrue;
}

//=================
//G_Teams_JoinAnyTeam - find us a team since we are too lazy to do ourselves
//=================
qboolean G_Teams_JoinAnyTeam( edict_t *ent, qboolean silent )
{
	int best = game.maxclients+1;
	int i, team = -1;
	qboolean wasinqueue = ( ent->r.client->pers.queueTimeStamp );

	G_Teams_UpdateMembersList(); // make sure we have up-to-date data

	//depending on the gametype, of course
	if( !GS_Gametype_IsTeamBased( game.gametype ) )
	{
		if( ent->s.team == TEAM_PLAYERS )
		{
			if( !silent )
			{
				G_PrintMsg( ent, "You are already in %s team\n", GS_TeamName( TEAM_PLAYERS ) );
			}
			return qfalse;
		}
		if( G_Teams_JoinTeam( ent, TEAM_PLAYERS ) )
		{
			if( !silent )
			{
				G_PrintMsg( NULL, "%s%s joined the %s team.\n",
				           ent->r.client->pers.netname, S_COLOR_WHITE, GS_TeamName( ent->s.team ) );
			}
		}
		return qtrue;

	}
	else
	{       //team based

		//find the available team with smaller player count
		for( i = TEAM_ALPHA; i < TEAM_ALPHA + g_maxteams->integer; i++ )
		{
			if( G_GameTypes_DenyJoinTeam( ent, i ) )
			{
				continue;
			}

			if( teamlist[i].numplayers < best )
			{
				best = teamlist[i].numplayers;
				team = i;
			}
		}

		if( team == ent->s.team )
		{                   // he is at the right team
			if( !silent )
			{
				G_PrintMsg( ent, "%sCouldn't find an emptier team than team %s.\n",
				           S_COLOR_WHITE, GS_TeamName( ent->s.team ) );
			}
			return qfalse;
		}

		if( team != -1 )
		{
			if( G_Teams_JoinTeam( ent, team ) )
			{
				if( !silent )
				{
					G_PrintMsg( NULL, "%s%s joined the %s team.\n",
					           ent->r.client->pers.netname, S_COLOR_WHITE, GS_TeamName( ent->s.team ) );
				}
				return qtrue;
			}
		}
		if( match.state <= MATCH_STATE_PLAYTIME && !silent )
			G_Teams_JoinChallengersQueue( ent );
	}

	// don't print message if we joined the queue
	if( !silent && ( !G_Gametype_hasChallengersQueue( game.gametype ) || wasinqueue ||
	                 !ent->r.client->pers.queueTimeStamp ) )
		G_PrintMsg( ent, "You can't join the game now\n" );

	return qfalse;
}

//=================
//G_Teams_Join_Cmd
//=================
void G_Teams_Join_Cmd( edict_t *ent )
{
	char *t;
	int team;

	t = trap_Cmd_Argv( 1 );
	if( !t || *t == 0 )
	{
		G_Teams_JoinAnyTeam( ent, qfalse );
		return;
	}

	team = GS_Teams_TeamFromName( t );
	if( team != -1 )
	{
		if( team == TEAM_SPECTATOR )
		{                      // special handling for spectator team
			Cmd_Spec_f( ent );
			return;
		}
		if( team == ent->s.team )
		{
			G_PrintMsg( ent, "You are already in %s team\n", GS_TeamName( team ) );
			return;
		}
		if( G_Teams_JoinTeam( ent, team ) )
		{                             //found a team to join
			G_PrintMsg( NULL, "%s%s joined the %s%s team.\n", ent->r.client->pers.netname, S_COLOR_WHITE,
			            GS_TeamName( ent->s.team ), S_COLOR_WHITE );
			return;
		}
	}
	else
	{
		G_PrintMsg( ent, "No such team.\n" );
		return;
	}
}

//======================================================================
//
//TEAM TAB
//
//======================================================================
static void LocationName( vec3_t origin, char *buf, int buflen );
static int  LocationTAG( char *name );

//==================
//G_Teams_TDM_UpdateTeamInfoMessages
//==================
void G_Teams_UpdateTeamInfoMessages( void )
{
	static int nexttime = 0;
	static char teammessage[MAX_STRING_CHARS];
	edict_t	*ent, *e;
	size_t len;
	int i, j, team;
	char entry[MAX_TOKEN_CHARS];
	char scratch[MAX_TOKEN_CHARS];
	int locationTag;

	nexttime -= game.snapFrameTime;
	if( nexttime > 0 )
		return;

	while( nexttime <= 0 )
		nexttime += 2000;

	// time for a new update

	for( team = TEAM_ALPHA; team < GS_MAX_TEAMS; team++ )
	{
		*teammessage = 0;
		Q_snprintfz( teammessage, sizeof( teammessage ), "ti \"" );
		len = strlen( teammessage );

		// add our team info to the string
		for( i = 0; teamlist[team].playerIndices[i] != -1; i++ )
		{
			ent = game.edicts + teamlist[team].playerIndices[i];

			if( game.gametype == GAMETYPE_CA && G_IsDead( ent ) )  // *NOT* show dead players
				continue;

			if( ent->r.client->is_coach )  // don't show coachs
				continue;

			// get location name
			*scratch = 0;
			LocationName( ent->s.origin, scratch, sizeof( scratch ) );
			locationTag = LocationTAG( scratch );
			if( locationTag == -1 )
				continue;

			*entry = 0;
			Q_snprintfz( entry, sizeof( entry ), "%i %i %i %i ", PLAYERNUM( ent ), locationTag, HEALTH_TO_INT( ent->health ), ARMOR_TO_INT( ent->r.client->armor ) );

			if( MAX_STRING_CHARS - len > strlen( entry ) )
			{
				Q_strncatz( teammessage, entry, sizeof( teammessage ) );
				len = strlen( teammessage );
			}
		}

		// add closing quote
		*entry = 0;
		Q_snprintfz( entry, sizeof( entry ), "\"" );
		if( MAX_STRING_CHARS - len > strlen( entry ) )
		{
			Q_strncatz( teammessage, entry, sizeof( teammessage ) );
			len = strlen( teammessage );
		}

		for( i = 0; teamlist[team].playerIndices[i] != -1; i++ )
		{
			ent = game.edicts + teamlist[team].playerIndices[i];

			if( !ent->r.inuse || !ent->r.client )
				continue;

			trap_GameCmd( ent, teammessage );

			// see if there are spectators chasing this player and send them the layout too
			for( j = 0; teamlist[TEAM_SPECTATOR].playerIndices[j] != -1; j++ )
			{
				e = game.edicts + teamlist[TEAM_SPECTATOR].playerIndices[j];

				if( !e->r.inuse || !e->r.client )
					continue;

				if( e->r.client->chase.active && e->r.client->chase.target == ENTNUM( ent ) )
				{
					trap_GameCmd( e, teammessage );
				}
			}
		}
	}
}


//======================================================================
//
// CHALLENGERS QUEUE
//
//======================================================================

//=================
//G_Teams_BestInChallengersQueue
//=================
edict_t *G_Teams_BestInChallengersQueue( unsigned int lastTimeStamp, edict_t *ignore )
{
	edict_t *e, *best = NULL;
	unsigned int bestTimeStamp = game.realtime+10000;

	// fill the teams with the players with the lowest timestamps
	for( e = game.edicts + 1; PLAYERNUM( e ) < game.maxclients; e++ )
	{
		if( !e->r.inuse || !e->r.client || !e->r.client->pers.connected )
			continue;
		if( !e->r.client->pers.queueTimeStamp || e->s.team != TEAM_SPECTATOR )
			continue;
		if( trap_GetClientState( PLAYERNUM( e ) ) < CS_SPAWNED || e->r.client->pers.connecting )
			continue;
		if( e == ignore )
			continue;
		if( e->r.client->pers.queueTimeStamp >= lastTimeStamp )
		{
			if( e->r.client->pers.queueTimeStamp < bestTimeStamp )
			{
				bestTimeStamp = e->r.client->pers.queueTimeStamp;
				best = e;
			}
		}
	}

	return best;
}

//=================
//G_Teams_ExecuteChallengersQueue
//=================
void G_Teams_ExecuteChallengersQueue( void )
{
	edict_t *ent;
	qboolean restartmatch = qfalse;

	// Medar fixme: this is only really makes sense, if playerlimit per team is one
	if( match.state == MATCH_STATE_PLAYTIME )
		return;

	if( !G_Gametype_hasChallengersQueue( game.gametype ) )
		return;

	if( game.realtime < level.spawnedTimeStamp + G_CHALLENGERS_MIN_JOINTEAM_MAPTIME )
	{
		static int time, lasttime;
		time = (int)( ( G_CHALLENGERS_MIN_JOINTEAM_MAPTIME - ( game.realtime - level.spawnedTimeStamp ) )*0.001 );
		if( lasttime && time == lasttime )
			return;
		lasttime = time;
		if( lasttime )
			G_CenterPrintMsg( NULL, "Waiting... %i", lasttime );
		else
			G_CenterPrintMsg( NULL, "" );
		return;
	}

	// pick players in join order and try to put them in the
	// game until we get the first refused one.
	ent = G_Teams_BestInChallengersQueue( 0, NULL );
	while( ent && G_Teams_JoinAnyTeam( ent, qtrue ) )
	{
		// if we successfully execute the challengers queue during the countdown, revert to warmup
		if( match.state == MATCH_STATE_COUNTDOWN )
		{
			restartmatch = qtrue;
		}

		ent = G_Teams_BestInChallengersQueue( ent->r.client->pers.queueTimeStamp, ent );
	}

	if( restartmatch == qtrue )
	{
		G_Match_Autorecord_Cancel();
		match.state = MATCH_STATE_WARMUP - 1;
		G_Match_SetUpNextState();
	}
}

//=================
////G_Teams_BestScoreBelow
//=================
static edict_t *G_Teams_BestScoreBelow( int maxscore )
{
	int team, i;
	edict_t *e, *best = NULL;
	int bestScore = -9999999;

	if( GS_Gametype_IsTeamBased( game.gametype ) )
	{
		for( team = TEAM_ALPHA; team < TEAM_ALPHA + g_maxteams->integer; team++ )
		{
			for( i = 0; teamlist[team].playerIndices[i] != -1; i++ )
			{
				e = game.edicts + teamlist[team].playerIndices[i];
				if( match.scores[PLAYERNUM( e )].score > bestScore &&
				    match.scores[PLAYERNUM( e )].score <= maxscore
				    && !e->r.client->pers.queueTimeStamp )
				{
					bestScore = match.scores[PLAYERNUM( e )].score;
					best = e;
				}
			}
		}
	}
	else
	{
		for( i = 0; teamlist[TEAM_PLAYERS].playerIndices[i] != -1; i++ )
		{
			e = game.edicts + teamlist[TEAM_PLAYERS].playerIndices[i];
			if( match.scores[PLAYERNUM( e )].score > bestScore &&
			    match.scores[PLAYERNUM( e )].score <= maxscore
			    && !e->r.client->pers.queueTimeStamp )
			{
				bestScore = match.scores[PLAYERNUM( e )].score;
				best = e;
			}
		}
	}

	return best;
}

//=================
//G_Teams_AdvanceChallengersQueue
//=================
void G_Teams_AdvanceChallengersQueue( void )
{
	int i, team, loserscount, winnerscount, playerscount = 0;
	int maxscore = 999999;
	edict_t *won, *e;
	int START_TEAM = TEAM_PLAYERS, END_TEAM = TEAM_PLAYERS+1;

	if( !G_Gametype_hasChallengersQueue( game.gametype ) )
		return;

	G_Teams_UpdateMembersList();

	if( GS_Gametype_IsTeamBased( game.gametype ) )
	{
		START_TEAM = TEAM_ALPHA;
		END_TEAM = TEAM_ALPHA + g_maxteams->integer;
	}

	// assign new timestamps to all the players inside teams
	for( team = START_TEAM; team < END_TEAM; team++ )
	{
		playerscount += teamlist[team].numplayers;
	}

	if( !playerscount )
		return;

	loserscount = 0;
	if( playerscount > 1 )
	{
		loserscount = (int)( playerscount / 2 );
	}
	winnerscount = playerscount - loserscount;

	// put everyone who just played out of the challengers queue
	for( team = START_TEAM; team < END_TEAM; team++ )
	{
		for( i = 0; teamlist[team].playerIndices[i] != -1; i++ )
		{
			e = game.edicts + teamlist[team].playerIndices[i];
			e->r.client->pers.queueTimeStamp = 0;
		}
	}

	// put (back) the best scoring players in first positions of challengers queue
	for( i = 0; i < winnerscount; i++ )
	{
		won = G_Teams_BestScoreBelow( maxscore );
		if( won )
		{
			maxscore = match.scores[PLAYERNUM( won )].score;
			won->r.client->pers.queueTimeStamp = 1 + ( winnerscount-i ); // never have 2 players with the same timestamp
		}
	}
}

//=================
//G_Teams_LeaveChallengersQueue
//=================
void G_Teams_LeaveChallengersQueue( edict_t *ent )
{
	if( !G_Gametype_hasChallengersQueue( game.gametype ) )
	{
		ent->r.client->pers.queueTimeStamp = 0;
		return;
	}

	if( ent->s.team != TEAM_SPECTATOR )
		return;

	// exit the challengers queue
	if( ent->r.client->pers.queueTimeStamp )
	{
		ent->r.client->pers.queueTimeStamp = 0;
		G_PrintMsg( ent, "%sYou left the challengers queue\n", S_COLOR_CYAN );
		G_UpdatePlayerMatchMsg( ent );
	}
}

//=================
//G_Teams_JoinChallengersQueue
//=================
void G_Teams_JoinChallengersQueue( edict_t *ent )
{
	int pos = 0;
	edict_t *e;

	if( !G_Gametype_hasChallengersQueue( game.gametype ) )
	{
		ent->r.client->pers.queueTimeStamp = 0;
		return;
	}

	if( ent->s.team != TEAM_SPECTATOR )
		return;

	// enter the challengers queue
	if( !ent->r.client->pers.queueTimeStamp )
	{                                       // enter the line
		ent->r.client->pers.queueTimeStamp = game.realtime;
		for( e = game.edicts + 1; PLAYERNUM( e ) < game.maxclients; e++ )
		{
			if( !e->r.inuse || !e->r.client || !e->r.client->pers.connected )
				continue;
			if( !e->r.client->pers.queueTimeStamp || e->s.team != TEAM_SPECTATOR )
				continue;
			// if there are other players with the same timestamp, increase ours
			if( e->r.client->pers.queueTimeStamp >= ent->r.client->pers.queueTimeStamp )
				ent->r.client->pers.queueTimeStamp = e->r.client->pers.queueTimeStamp+1;
			if( e->r.client->pers.queueTimeStamp < ent->r.client->pers.queueTimeStamp )
				pos++;
		}
		G_PrintMsg( ent, "%sYou entered the challengers queue in position %i\n", S_COLOR_CYAN, pos+1 );
		G_UpdatePlayerMatchMsg( ent );
	}
}

//======================================================================
//
//TEAM COMMUNICATIONS
//
//======================================================================

static edict_t *point;
static vec3_t point_location;

//======================================================================
//	LOCATIONS
//======================================================================

void G_RegisterMapLocationName( char *name )
{
	int i;
	char temp[MAX_CONFIGSTRING_CHARS];

	if( !name )
		return;

	for( i = 0; i < level.numLocations; i++ )
	{
		if( !Q_stricmp( name, level.locationNames[i] ) )
			return;
	}

	Q_strncpyz( temp, name, sizeof( temp ) );
	level.locationNames[level.numLocations] = G_LevelCopyString( temp );
	level.numLocations++;
}

int LocationTAG( char *name )
{
	int i;

	if( !level.numLocations )
		return -1;

	for( i = 0; i < level.numLocations; i++ )
	{
		if( !Q_stricmp( name, level.locationNames[i] ) )
			return i;
	}

	return 0;
}

void LocationName( vec3_t origin, char *buf, int buflen )
{
	edict_t *what = NULL;
	edict_t *hot = NULL;
	float hotdist = 3.0f*8192.0f*8192.0f;
	vec3_t v;

	while( ( what = G_Find( what, FOFS( classname ), "target_location" ) ) != NULL )
	{
		VectorSubtract( what->s.origin, origin, v );

		if( VectorLengthFast( v ) > hotdist )
		{
			continue;
		}

		if( !trap_inPVS( what->s.origin, origin ) )
			continue;

		hot = what;
		hotdist = VectorLengthFast( v );
	}

	if( !hot || !hot->message )
		Q_snprintfz( buf, buflen, "someplace" );
	else if( hot->count > 0 && hot->count < 10 )
		Q_snprintfz( buf, buflen, "%c%c%s", Q_COLOR_ESCAPE, hot->count + '0', hot->message );
	else
		Q_snprintfz( buf, buflen, "%s", hot->message );
}

static void UpdatePoint( edict_t *who )
{
	vec3_t angles, forward, diff;
	trace_t trace;
	edict_t *ent, *ent_best = NULL;
	int i, j;
	float value, value_best = 0.35f; // if nothing better is found, print nothing
	gclient_t *client = who->r.client;
	vec3_t boxpoints[8], viewpoint;

	AngleVectors( client->ps.viewangles, forward, NULL, NULL );
	VectorCopy( who->s.origin, viewpoint );
	viewpoint[2] += who->viewheight;

	for( i = 0; i < game.numentities; i++ )
	{
		ent = game.edicts + i;

		if( !ent->r.inuse || !ent->s.modelindex || ent == who )
			continue;
		if( ent->r.svflags & SVF_NOCLIENT || ent->r.solid == SOLID_NOT )
			continue;
		if( ent->s.type != ET_PLAYER && ent->s.type != ET_ITEM )
			continue;

		VectorSubtract( ent->s.origin, viewpoint, angles );
		VectorNormalize( angles );
		VectorSubtract( forward, angles, diff );
		for( j = 0; j < 3; j++ ) if( diff[j] < 0 ) diff[j] = -diff[j];
		value = VectorLengthFast( diff );

		if( value < value_best )
		{
			BuildBoxPoints( boxpoints, ent->s.origin, ent->r.mins, ent->r.maxs );
			for( j = 0; j < 8; j++ )
			{
				G_Trace( &trace, viewpoint, vec3_origin, vec3_origin, boxpoints[j], who, MASK_OPAQUE );
				if( trace.fraction == 1 )
				{
					value_best = value;
					ent_best = ent;
					break;
				}
			}
		}
	}

	if( ent_best != NULL )
	{
		point = ent_best;
		VectorCopy( ent_best->s.origin, point_location );
	}
	else
	{
		vec3_t dest;

		VectorMA( viewpoint, 8192, forward, dest );
		G_Trace( &trace, viewpoint, vec3_origin, vec3_origin, dest, who, MASK_OPAQUE );

		point = NULL;
		VectorCopy( trace.endpos, point_location );
	}
}

static void Say_Team_Location( edict_t *who, char *buf, int buflen, const char *current_color )
{
	LocationName( who->s.origin, buf, buflen );
	Q_strncatz( buf, current_color, buflen );
}

static void Say_Team_Armor( edict_t *who, char *buf, int buflen, const char *current_color )
{
	if( who->r.client->armortag )
	{
		Q_snprintfz( buf, buflen, "%s%i%s", game.items[who->r.client->armortag]->color,
		             ARMOR_TO_INT( who->r.client->armor ), current_color );
	}
	else
	{
		Q_snprintfz( buf, buflen, "%s0%s", S_COLOR_GREEN, current_color );
	}
}

static void Say_Team_Health( edict_t *who, char *buf, int buflen, const char *current_color )
{
	int health = HEALTH_TO_INT( who->health );

	if( health <= 0 )
		Q_snprintfz( buf, buflen, "%s0%s", S_COLOR_RED, current_color );
	else if( health <= 50 )
		Q_snprintfz( buf, buflen, "%s%i%s", S_COLOR_YELLOW, health, current_color );
	else if( health <= 100 )
		Q_snprintfz( buf, buflen, "%s%i%s", S_COLOR_WHITE, health, current_color );
	else
		Q_snprintfz( buf, buflen, "%s%i%s", S_COLOR_GREEN, health, current_color );
}

static void WeaponString( edict_t *who, int weapon, char *buf, int buflen, const char *current_color )
{
	weapon_info_t *weaponinfo;
	int strong_ammo, weak_ammo;

	Q_snprintfz( buf, buflen, "%s%s%s", ( game.items[weapon]->color ? game.items[weapon]->color : "" ),
	             game.items[weapon]->short_name, current_color );

	weaponinfo = &gs_weaponInfos[weapon];
	strong_ammo = who->r.client->inventory[weaponinfo->firedef->ammo_id];
	weak_ammo = who->r.client->inventory[weaponinfo->firedef_weak->ammo_id];

	if( weapon == WEAP_GUNBLADE )
		Q_strncatz( buf, va( ":%i", strong_ammo ), buflen );
	else if( strong_ammo > 0 )
		Q_strncatz( buf, va( ":%i/%i", strong_ammo, weak_ammo ), buflen );
	else
		Q_strncatz( buf, va( ":%i", weak_ammo ), buflen );
}

static qboolean HasItem( edict_t *who, int item )
{
	return ( who->r.client->inventory[item] );
}

static void Say_Team_Best_Weapons( edict_t *who, char *buf, int buflen, const char *current_color )
{
	char weapon_strings[2][20];
	int weap, printed = 0;

	for( weap = WEAP_TOTAL-1; weap > WEAP_GUNBLADE; weap-- )
	{
		// evil hack to make RL more important than PG
		if( weap == WEAP_PLASMAGUN ) weap = WEAP_ROCKETLAUNCHER;
		else if( weap == WEAP_ROCKETLAUNCHER ) weap = WEAP_PLASMAGUN;

		if( HasItem( who, weap ) )
		{
			WeaponString( who, weap, weapon_strings[printed], sizeof( weapon_strings[printed] ), current_color );
			if( ++printed == 2 )
				break;
		}

		if( weap == WEAP_PLASMAGUN ) weap = WEAP_ROCKETLAUNCHER;
		else if( weap == WEAP_ROCKETLAUNCHER ) weap = WEAP_PLASMAGUN;
	}

	if( printed == 2 )
	{
		Q_snprintfz( buf, buflen, "%s%s %s%s", weapon_strings[1], current_color, weapon_strings[0], current_color );
	}
	else if( printed == 1 )
	{
		Q_snprintfz( buf, buflen, "%s%s", weapon_strings[0], current_color );
	}
	else
	{
		WeaponString( who, WEAP_GUNBLADE, buf, buflen, current_color );
		Q_strncatz( buf, current_color, buflen );
	}
}

static void Say_Team_Current_Weapon( edict_t *who, char *buf, int buflen, const char *current_color )
{
	if( !who->s.weapon )
	{
		buf[0] = 0;
		return;
	}

	WeaponString( who, who->s.weapon, buf, buflen, current_color );
	Q_strncatz( buf, current_color, buflen );
}

static void Say_Team_Point( edict_t *who, char *buf, int buflen, const char *current_color )
{
	if( !point )
	{
		Q_snprintfz( buf, buflen, "nothing" );
		return;
	}

	if( point->s.type == ET_ITEM )
	{
		gitem_t *item = GS_FindItemByClassname( point->classname );
		if( item )
			Q_snprintfz( buf, buflen, "%s%s%s", ( item->color ? item->color : "" ), item->short_name, current_color );
		else
			Q_snprintfz( buf, buflen, point->classname );
	}
	else
	{
		Q_snprintfz( buf, buflen, "%s%s", point->classname, current_color );
	}
}

static void Say_Team_Point_Location( edict_t *who, char *buf, int buflen, const char *current_color )
{
	LocationName( point_location, buf, buflen );
	Q_strncatz( buf, current_color, buflen );
}

static void Say_Team_Pickup( edict_t *who, char *buf, int buflen, const char *current_color )
{
	if( !who->r.client->resp.last_pickup )
	{
		buf[0] = 0;
	}
	else
	{
		gitem_t *item = GS_FindItemByClassname( who->r.client->resp.last_pickup->classname );
		if( item )
			Q_snprintfz( buf, buflen, "%s%s%s", ( item->color ? item->color : "" ), item->short_name, current_color );
		else
			buf[0] = 0;
	}
}

static void Say_Team_Pickup_Location( edict_t *who, char *buf, int buflen, const char *current_color )
{
	if( !who->r.client->resp.last_pickup )
	{
		buf[0] = 0;
	}
	else
	{
		LocationName( who->r.client->resp.last_pickup->s.origin, buf, buflen );
		Q_strncatz( buf, current_color, buflen );
	}
}

static void Say_Team_Drop( edict_t *who, char *buf, int buflen, const char *current_color )
{
	gitem_t *item = who->r.client->resp.last_drop_item;

	if( !item )
		buf[0] = 0;
	else
		Q_snprintfz( buf, buflen, "%s%s%s", ( item->color ? item->color : "" ), item->short_name, current_color );
}

static void Say_Team_Drop_Location( edict_t *who, char *buf, int buflen, const char *current_color )
{
	if( !who->r.client->resp.last_drop_item )
	{
		buf[0] = 0;
	}
	else
	{
		LocationName( who->r.client->resp.last_drop_location, buf, buflen );
		Q_strncatz( buf, current_color, buflen );
	}
}

static void G_Say_Team_Spectator( edict_t *who, const char *msg )
{
	edict_t *cl_ent;
	int i;

	for( i = 0; i < game.maxclients; i++ )
	{
		cl_ent = game.edicts + 1 + i;

		if( !cl_ent->r.inuse )
			continue;

		if( cl_ent->s.team == who->s.team )
		{
			G_ChatMsg( cl_ent, "%s[SPEC]%s %s%s: %s\n", S_COLOR_YELLOW, S_COLOR_WHITE, who->r.client->pers.netname,
			           S_COLOR_YELLOW, msg );
		}
	}
}

void G_Say_Team( edict_t *who, char *msg, qboolean checkflood )
{
	char outmsg[256];
	char buf[256];
	int i;
	char *p;
	edict_t *cl_ent;
	char current_color[3];

	if( who->s.team != TEAM_SPECTATOR && ( !GS_Gametype_IsTeamBased( game.gametype ) || game.gametype == GAMETYPE_DUEL ) )
	{
		Cmd_Say_f( who, qfalse, qtrue );
		return;
	}

	if( checkflood )
	{
		if( CheckFlood( who, qtrue ) )
			return;
	}

	if( *msg == '\"' )
	{
		msg[strlen( msg ) - 1] = 0;
		msg++;
	}

	if( who->s.team == TEAM_SPECTATOR )
	{
		G_Say_Team_Spectator( who, msg );
		return;
	}

	Q_strncpyz( current_color, S_COLOR_WHITE, sizeof( current_color ) );

	memset( outmsg, 0, sizeof( outmsg ) );

	UpdatePoint( who );

	for( p = outmsg; *msg && ( p - outmsg ) < sizeof( outmsg ) - 3; msg++ )
	{
		if( *msg == '%' )
		{
			buf[0] = 0;
			switch( *++msg )
			{
			case 'l':
				Say_Team_Location( who, buf, sizeof( buf ), current_color );
				break;
			case 'a':
				Say_Team_Armor( who, buf, sizeof( buf ), current_color );
				break;
			case 'h':
				Say_Team_Health( who, buf, sizeof( buf ), current_color );
				break;
			case 'b':
				Say_Team_Best_Weapons( who, buf, sizeof( buf ), current_color );
				break;
			case 'w':
				Say_Team_Current_Weapon( who, buf, sizeof( buf ), current_color );
				break;
			case 'x':
				Say_Team_Point( who, buf, sizeof( buf ), current_color );
				break;
			case 'y':
				Say_Team_Point_Location( who, buf, sizeof( buf ), current_color );
				break;
			case 'X':
				Say_Team_Pickup( who, buf, sizeof( buf ), current_color );
				break;
			case 'Y':
				Say_Team_Pickup_Location( who, buf, sizeof( buf ), current_color );
				break;
			case 'd':
				Say_Team_Drop( who, buf, sizeof( buf ), current_color );
				break;
			case 'D':
				Say_Team_Drop_Location( who, buf, sizeof( buf ), current_color );
				break;
			case '%':
				*p++ = *msg;
				break;
			default:
				// Maybe add a warning here?
				*p++ = '%';
				*p++ = *msg;
			}
			if( strlen( buf ) + ( p - outmsg ) < sizeof( outmsg ) - 3 )
			{
				Q_strncatz( outmsg, buf, sizeof( outmsg ) );
				p += strlen( buf );
			}
		}
		else if( *msg == '^' )
		{
			*p++ = *msg++;
			*p++ = *msg;
			Q_strncpyz( current_color, p-2, sizeof( current_color ) );
		}
		else
		{
			*p++ = *msg;
		}
	}
	*p = 0;

	for( i = 0; i < game.maxclients; i++ )
	{
		cl_ent = game.edicts + 1 + i;

		if( !cl_ent->r.inuse )
			continue;

		if( cl_ent->s.team == who->s.team )
		{
			G_ChatMsg( cl_ent, "%s[TEAM]%s %s%s: %s\n", S_COLOR_YELLOW, S_COLOR_WHITE, who->r.client->pers.netname,
			           S_COLOR_YELLOW, outmsg );
		}
	}
}

// coach

void G_Teams_Coach( edict_t *ent )
{
	if( GS_Gametype_IsTeamBased( game.gametype ) && game.gametype != GAMETYPE_DUEL && ent->s.team != TEAM_SPECTATOR )
	{
		if( !teamlist[ent->s.team].has_coach )
		{
			if( match.state > MATCH_STATE_WARMUP && !gtimeout.active )
			{
				G_PrintMsg( ent, "Can't set coach mode with the match in progress\n" );
			}
			else
			{
				// move to coach mode
				ent->r.client->is_coach = qtrue;
				ent->health = ent->max_health;
				ent->s.modelindex = ent->s.modelindex2 = ent->s.weapon = 0;
				ent->deadflag = DEAD_NO;
				ent->movetype = MOVETYPE_NOCLIP;
				ent->r.solid = SOLID_NOT;
				ent->r.svflags |= SVF_NOCLIENT;
				GClip_LinkEntity( ent );
				ent->r.client->chase.active = qtrue;
				ent->r.client->chase.teamonly = qtrue;
				ent->r.client->chase.target = ENTNUM( ent ); // set to self for start
				ent->r.client->ps.pmove.pm_type = PM_CHASECAM;
				memset( &ent->snap, 0, sizeof( ent->snap ) );
				G_ChasePlayer( ent, NULL, qtrue, 0 );
				if( ent->r.client->chase.target == ENTNUM( ent ) )
					G_CenterPrintMsg( ent, "No one to chase" );

				//clear up his scores
				G_Match_Ready( ent ); // set ready and check readys
				match.scores[PLAYERNUM( ent )].score = 0;
				match.scores[PLAYERNUM( ent )].deaths = 0;
				match.scores[PLAYERNUM( ent )].kills = 0;
				match.scores[PLAYERNUM( ent )].suicides = 0;

				teamlist[ent->s.team].has_coach = qtrue;
				G_PrintMsg( NULL, "%s%s is now team %s coach \n", ent->r.client->pers.netname,
				           S_COLOR_WHITE, GS_TeamName( ent->s.team ) );
			}
		}
		else if( ent->r.client->is_coach )
		{                            // if you are this team coach, resign
			ent->r.client->is_coach = qfalse;
			G_PrintMsg( NULL, "%s%s is no longer team %s coach \n", ent->r.client->pers.netname,
			           S_COLOR_WHITE, GS_TeamName( ent->s.team ) );

			G_Teams_SetTeam( ent, ent->s.team );
		}
		else
			G_PrintMsg( ent, "Your team already has a coach.\n" );
	}
	else
		G_PrintMsg( ent, "Coaching only valid while on a team in Team based Gametypes.\n" );
}

void G_Teams_CoachLockTeam( edict_t *ent )
{
	if( ent->r.client->is_coach )
	{
		if( !G_Teams_TeamIsLocked( ent->s.team ) )
		{
			G_Teams_LockTeam( ent->s.team );
			G_PrintMsg( NULL, "%s%s locked the %s team.\n", ent->r.client->pers.netname,
			           S_COLOR_WHITE, GS_TeamName( ent->s.team ) );
		}
	}
}

void G_Teams_CoachUnLockTeam( edict_t *ent )
{
	if( ent->r.client->is_coach )
	{
		if( G_Teams_TeamIsLocked( ent->s.team ) )
		{
			G_Teams_UnLockTeam( ent->s.team );
			G_PrintMsg( NULL, "%s%s unlocked the %s team.\n", ent->r.client->pers.netname,
			           S_COLOR_WHITE, GS_TeamName( ent->s.team ) );
		}
	}
}

void G_Teams_CoachRemovePlayer( edict_t *ent )
{
	if( ent->r.client->is_coach )
	{
		char *text;
		edict_t *toremove;

		if( !ent->r.inuse || !ent->r.client )
			return;

		text = trap_Cmd_Argv( 1 );

		if( !text || !strlen( text ) )
		{
			G_PrintMsg( ent, "Usage: remove <player>\n"
			            " use 'players' to list players\n" );
			return;
		}

		toremove = G_PlayerForText( text );

		if( !toremove )
		{
			G_PrintMsg( ent, "No such player.\n" );
			return;
		}
		else if( toremove->s.team == ent->s.team )
		{
			G_PrintMsg( NULL, "%s^7 removed %s^7 from %s team.\n", ent->r.client->pers.netname,
			           toremove->r.client->pers.netname, GS_TeamName( toremove->s.team ) );
			Cmd_Spec_f( toremove );
		}
		else
			G_PrintMsg( ent, "You can only remove teammates\n" );

		return;
	}
	else
		G_PrintMsg( ent, "You must be coach to use this command\n" );
}
