/*
   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"
#include "g_callvotes.h"

// Generic functions used for all gametypes when don't require any special setting

static void G_Gametype_GENERIC_SetUpWarmup( void )
{
	match.pickableItemsMask = GS_Gametype_SpawnableItemMask( game.gametype )|GS_Gametype_DropableItemMask( game.gametype );
	if( g_instagib->integer )
	{
		match.pickableItemsMask &= ~G_INSTAGIB_NEGATE_ITEMMASK;
	}

	// wsw: Medar: fixme: when countdown is aborted we shouldn't respawn clients
	// I put this here, so connecting before warmup state doesn't cause problems
	G_Match_RespawnAllClients();

	if( GS_Gametype_IsTeamBased( game.gametype ) )
	{
		qboolean any = qfalse;
		int team;
		for( team = TEAM_ALPHA; team < TEAM_ALPHA + g_maxteams->integer; team++ )
		{
			if( G_Teams_TeamIsLocked( team ) )
			{
				G_Teams_UnLockTeam( team );
				any = qtrue;
			}
		}
		if( any )
			G_PrintMsg( NULL, "Teams unlocked.\n" );
	}
	else
	{
		if( G_Teams_TeamIsLocked( TEAM_PLAYERS ) )
		{
			G_Teams_UnLockTeam( TEAM_PLAYERS );
			G_PrintMsg( NULL, "Teams unlocked.\n" );
		}
	}
	G_Teams_RemoveInvites();
}

void G_Gametype_GENERIC_SetUpCountdown( void )
{
	edict_t *ent;

	G_Match_RemoveAllClientLasers();
	G_Match_RemoveAllProjectiles();
	G_Match_RespawnAllItems();

	if( game.lock_teams )
	{
		if( GS_Gametype_IsTeamBased( game.gametype ) )
		{
			int team;
			for( team = TEAM_ALPHA; team < TEAM_ALPHA + g_maxteams->integer; team++ )
				G_Teams_LockTeam( team );
		}
		else
		{
			G_Teams_LockTeam( TEAM_PLAYERS );
		}
		G_PrintMsg( NULL, "Teams locked.\n" );
	}

	match.pickableItemsMask = 0; // disallow item pickup
	G_AnnouncerSound( NULL, trap_SoundIndex( va( S_ANNOUNCER_COUNTDOWN_GET_READY_TO_FIGHT_1_to_2, ( rand()&1 )+1 ) ),
	                  GS_MAX_TEAMS, qtrue );

	// clear client->resp before match
	for( ent = game.edicts + 1; PLAYERNUM( ent ) < game.maxclients; ent++ )
	{
		if( !ent->r.inuse || ent->s.team == TEAM_SPECTATOR )
			continue;
		if( ent->s.weapon == WEAP_LASERGUN )
			G_HideClientLaser( ent );

		InitClientResp( ent->r.client );
	}
}

void G_Gametype_GENERIC_SetUpMatch( void )
{
	int i;

	//clear stats and scores
	memset( match.scores, 0, sizeof( client_scores_t ) * MAX_CLIENTS );
	for( i = TEAM_PLAYERS; i < GS_MAX_TEAMS; i++ )
	{
		teamlist[i].teamplayerscores = 0;
		teamlist[i].teamscore = 0;
	}

	match.pickableItemsMask =
	        GS_Gametype_SpawnableItemMask( game.gametype )|GS_Gametype_DropableItemMask( game.gametype );
	if( g_instagib->integer )
	{
		match.pickableItemsMask &= ~G_INSTAGIB_NEGATE_ITEMMASK;
		/*if( game.gametype == GAMETYPE_TDM )
			match.pickableItemsMask |= IT_CAPTURE_AREA;*/
	}

	G_Match_SetUpDelayedItems();
	G_Match_RespawnAllClients();
	G_Match_FreeBodyQueue();

	G_AnnouncerSound( NULL, trap_SoundIndex( va( S_ANNOUNCER_COUNTDOWN_FIGHT_1_to_2, ( rand()&1 )+1 ) ), GS_MAX_TEAMS,
	                  qtrue );
	G_CenterPrintMsg( NULL, "FIGHT!\n" );
}

void G_Gametype_GENERIC_SetUpEndMatch( void )
{
	edict_t *ent;

	//check for extended time
	if( GS_Gametype_IsTeamBased( game.gametype ) && !match.forceExit )
	{
		if( G_Match_Tied() )
		{
			match.state = MATCH_STATE_PLAYTIME;
			if( g_match_extendedtime->value )
			{
				if( match.extendedTime == 0 )  // first one
					G_AnnouncerSound( NULL, trap_SoundIndex( S_ANNOUNCER_OVERTIME_GOING_TO_OVERTIME ), GS_MAX_TEAMS, qtrue );
				else
					G_AnnouncerSound( NULL, trap_SoundIndex( S_ANNOUNCER_OVERTIME_OVERTIME ), GS_MAX_TEAMS, qtrue );
				// wsw: Medar: fixme: the message only shows whole minutes
				G_PrintMsg( NULL, "Match tied. Timelimit extended by %i minutes!\n", g_match_extendedtime->integer );
				G_CenterPrintMsg( NULL, "%i MINUTE OVERTIME\n", g_match_extendedtime->integer );
				match.endtime = level.time + (int)( ( fabs( g_match_extendedtime->value ) * 60 ) * 1000 );
			}
			else
			{
				G_AnnouncerSound( NULL, trap_SoundIndex( va( S_ANNOUNCER_OVERTIME_SUDDENDEATH_1_to_2, ( rand()&1 )+1 ) ),
				                  GS_MAX_TEAMS, qtrue );
				G_PrintMsg( NULL, "Match tied. Sudden death!\n" );
				G_CenterPrintMsg( NULL, "SUDDEN DEATH\n" );
				match.endtime = 0;
			}
			return;
		}
	}

	for( ent = game.edicts + 1; PLAYERNUM( ent ) < game.maxclients; ent++ )
	{
		if( ent->r.inuse && trap_GetClientState( PLAYERNUM( ent ) ) >= CS_SPAWNED )
			G_ClearPlayerStateEvents( ent->r.client );
	}

	G_AnnouncerSound( NULL, trap_SoundIndex( va( S_ANNOUNCER_POSTMATCH_GAMEOVER_1_to_2, ( rand()&1 )+1 ) ), GS_MAX_TEAMS,
	                  qtrue );
	match.pickableItemsMask = 0; // disallow item pickup

	G_GameType_BeginPostMatch();
}

//==========================================================
//				Gametypes definitions
//note that the enum in gs_public must follow the same order
//==========================================================
gametype_t gametypes[] =
{
	//GAMETYPE_DM
	{
		NULL,                                       //initialize
		NULL,                                       //start a new map
		G_Gametype_DM_CheckRules,                   //think frame
		ClientRespawn,                              //player respawn
		ClientBeginMultiplayerGame,                 //player entered the server
		G_Gametype_DM_ScoreboardMessage,
		G_Gametype_GENERIC_SetUpWarmup,             //warmup
		G_Gametype_GENERIC_SetUpCountdown,          //countdown
		G_Gametype_GENERIC_SetUpMatch,              //start match
		G_Gametype_GENERIC_SetUpEndMatch,           //end match
		0,                                          //max players per team (0 = no limit)
		qfalse,                                     //has spectators queue line
		// default item respwan time
		20, // ammo_respawn
		25, // armor_respawn
		5, // weapon_respawn
		35, // health_respawn
		90, // powerup_respawn
		20, // megahealth_respawn
		// few default settings
		20, // timelimit
		0, // extended_time
		0, // scorelimit
		256,
	},

	//GAMETYPE_DUEL
	{
		NULL,                                       //initialize
		NULL,                                       //start a new map
		G_Gametype_DUEL_CheckRules,                 //think
		ClientRespawn,                              //player respawn
		ClientBeginMultiplayerGame,                 //player entered the server
		G_Gametype_DUEL_ScoreboardMessage,
		G_Gametype_GENERIC_SetUpWarmup,             //warmup
		G_Gametype_GENERIC_SetUpCountdown,          //countdown
		G_Gametype_GENERIC_SetUpMatch,              //start match
		G_Gametype_GENERIC_SetUpEndMatch,           //end match
		1,                                          //max players per team (0 = no limit)
		qtrue,                                      //has spectators queue line
		// default item respwan time
		20, // ammo_respawn
		25, // armor_respawn
		15, // weapon_respawn
		35, // health_respawn
		90, // powerup_respawn
		20, // megahealth_respawn
		// few default settings
		10, // timelimit
		2, // extended_time
		0, // scorelimit
		64,
	},

	//GAMETYPE_TDM
	{
		NULL,           //initialize
		G_Gametype_TDM_NewMap,                  //start a new map
		G_Gametype_TDM_CheckRules,                  //think
		ClientRespawn,                              //player respawn
		ClientBeginMultiplayerGame,                 //player entered the server
		G_Gametype_TDM_ScoreboardMessage,
		G_Gametype_GENERIC_SetUpWarmup,             //warmup
		G_Gametype_GENERIC_SetUpCountdown,          //countdown
		G_Gametype_TDM_SetUpMatch,                  //start match
		G_Gametype_GENERIC_SetUpEndMatch,           //end match
		0,                                          //max players per team (0 = no limit)
		qfalse,                                     //has spectators queue line
		// default item respwan time
		20, // ammo_respawn
		25, // armor_respawn
		15, // weapon_respawn
		35, // health_respawn
		90, // powerup_respawn
		20, // megahealth_respawn
		// few default settings
		20, // timelimit
		5, // extended_time
		0, // scorelimit
		256,
	},

	//GAMETYPE_CTF
	{
		G_Gametype_CTF_Init,                        //initialize gametype
		G_Gametype_CTF_NewMap,                      //start a new map
		G_Gametype_CTF_CheckRules,                  //think
		ClientRespawn,                              //player respawn
		G_Gametype_CTF_ClientBegin,                 //player entered the server
		G_Gametype_CTF_ScoreboardMessage,           //scoreboard
		G_Gametype_GENERIC_SetUpWarmup,             //warmup
		G_Gametype_GENERIC_SetUpCountdown,          //countdown
		G_Gametype_CTF_SetUpMatch,                  //start match
		G_Gametype_CTF_SetUpEndMatch,               //end match
		0,                                          //max players per team (0 = no limit)
		qfalse,                                     //has spectators queue line
		// default item respwan time
		20, // ammo_respawn
		25, // armor_respawn
		5, // weapon_respawn
		35, // health_respawn
		90, // powerup_respawn
		20, // megahealth_respawn
		// few default settings
		20, // timelimit
		5, // extended_time
		0, // scorelimit
		256,
	},

	//GAMETYPE_RACE
	{
		NULL,                                       //initialize gametype
		NULL,                                       //start a new map
		G_Gametype_RACE_CheckRules,                 //think
		G_Gametype_RACE_ClientRespawn,              //player respawn
		G_Gametype_RACE_ClientBegin,                //player entered the server
		G_Gametype_RACE_ScoreboardMessage,          //scoreboard
		G_Gametype_RACE_SetUpWarmup,                //warmup
		NULL,                                       //countdown
		G_Gametype_RACE_SetUpMatch,                 //start match
		G_Gametype_GENERIC_SetUpEndMatch,           //end match
		0,                                          //max players per team (0 = no limit)
		qfalse,                                     //has spectators queue line
		// default item respwan time
		20, // ammo_respawn
		25, // armor_respawn
		15, // weapon_respawn
		35, // health_respawn
		90, // powerup_respawn
		20, // megahealth_respawn
		// few default settings
		0, // timelimit
		0, // extended_time
		0, // scorelimit
		0,
	},

	//GAMETYPE_CA
	{
		NULL,                                       //initialize
		G_Gametype_CA_NewMap,                                       //start a new map
		G_Gametype_CA_CheckRules,                   //think
		G_Gametype_CA_ClientRespawn,                                //player respawn
		ClientBeginMultiplayerGame,                 //player entered the server
		G_Gametype_CA_ScoreboardMessage,
		G_Gametype_GENERIC_SetUpWarmup,             //warmup
		G_Gametype_CA_SetUpCountdown,           //countdown
		G_Gametype_CA_SetUpMatch,                   //start match
		G_Gametype_GENERIC_SetUpEndMatch,           //end match
		0,                                          //max players per team (0 = no limit)
		qfalse,                                     //has spectators queue line
		// default item respwan time
		20, // ammo_respawn
		25, // armor_respawn
		15, // weapon_respawn
		35, // health_respawn
		90, // powerup_respawn
		20, // megahealth_respawn
		// few default settings
		0, // timelimit
		0, // extended_time
		10, // scorelimit
		256,
	},

#ifdef DUEL_ARENA
	//GAMETYPE_DA
	{
		NULL,                                       //initialize
		NULL,                                       //start a new map
		G_Gametype_DA_CheckRules,                   //think
		G_Gametype_DA_ClientRespawn,                //player respawn
		ClientBeginMultiplayerGame,                 //player entered the server
		G_Gametype_DA_ScoreboardMessage,
		G_Gametype_GENERIC_SetUpWarmup,             //warmup
		G_Gametype_GENERIC_SetUpCountdown,          //countdown
		G_Gametype_GENERIC_SetUpMatch,              //start match
		G_Gametype_DA_SetUpEndMatch,            //end match
		1,                                          //max players per team (0 = no limit)
		qtrue,                                      //has spectators queue line
		// default item respwan time
		20, // ammo_respawn
		25, // armor_respawn
		15, // weapon_respawn
		35, // health_respawn
		90, // powerup_respawn
		20, // megahealth_respawn
		// few default settings
		0, // timelimit
		0, // extended_time
		10, // scorelimit
		128,
	},
#endif

	{ NULL }
};


//==========================================================
//					Matches
//==========================================================

cvar_t *g_warmup_enabled;
cvar_t *g_warmup_timelimit;
cvar_t *g_match_extendedtime;
cvar_t *g_countdown_time;
cvar_t *g_votable_gametypes;
cvar_t *g_scorelimit;
cvar_t *g_timelimit;
cvar_t *g_gametype;

matchgame_t match;

//==========================================================
//					Matches
//==========================================================

//=================
//G_GetMatchState
//
//The server asks for the match state data
//=================
void G_GetMatchState( match_state_t *matchstate )
{
	unsigned int timelimit;
	int clocktime;

	assert( matchstate );

	memset( matchstate, 0, sizeof( match_state_t ) );
	matchstate->state = match.state;
	matchstate->roundstate = match.roundstate;
	matchstate->paused = ( gtimeout.active != 0 );

	// timelimit
	if( match.endtime )
		timelimit = 1000*( match.endtime - match.starttime );
	else
		timelimit = 0;
	matchstate->timelimit = timelimit;
	matchstate->extendedtime = ( match.extendedTime > 0 );

	// match clock
	if( match.endtime )
	{
		clocktime = match.endtime - level.time;
	}
	else if( match.extendedTime > 0 )
	{
		// count down from one minute again and again
		unsigned int timepassed = level.time - match.starttime;
		clocktime = ( 60000 - (int)( timepassed ) % 60000 ) - ( timepassed - (int) timepassed );
	}
	else
	{
		clocktime = level.time - match.starttime;
	}

	if( clocktime < 0 )
		clocktime = 0;

	matchstate->clock_msecs = (unsigned int)( clocktime );
	if( matchstate->clock_msecs == 0 )
	{
		matchstate->clock_secs = matchstate->clock_mins = 0;
		return;
	}
	matchstate->clock_secs = (unsigned int)( clocktime / 1000 );
	matchstate->clock_msecs -= (unsigned int)( matchstate->clock_secs * 1000 );
	matchstate->clock_mins = (unsigned int)( matchstate->clock_secs / 60 );
	matchstate->clock_secs -= (unsigned int)( matchstate->clock_mins * 60 );
}

qboolean G_Match_Tied( void )
{
	int team, total, numteams;

	total = 0; numteams = 0;
	for( team = TEAM_ALPHA; team < TEAM_ALPHA + g_maxteams->integer; team++ )
	{
		if( !teamlist[team].numplayers )
			continue;

		numteams++;
		total += teamlist[team].teamscore;
	}

	if( numteams < 2 )
		return qfalse;
	else
	{

		// total / numteams = averaged score
		for( team = TEAM_ALPHA; team < TEAM_ALPHA + g_maxteams->integer; team++ )
		{
			if( !teamlist[team].numplayers )
				continue;

			if( teamlist[team].teamscore != total / numteams )
			{
				return qfalse;
			}
		}
	}

	return qtrue;
}

//=================
//G_Match_Autorecord_Start
//=================
void G_Match_Autorecord_Start( void )
{
	trap_GameCmd( NULL, "autr start" );

	if( g_autorecord->integer && game.gametype != GAMETYPE_RACE )
	{
		char name[MAX_STRING_CHARS], datetime[17], players[MAX_STRING_CHARS];
		time_t long_time;
		struct tm *newtime;

		// date & time
		time( &long_time );
		newtime = localtime( &long_time );

		Q_snprintfz( datetime, sizeof( datetime ), "%04d-%02d-%02d_%02d-%02d", newtime->tm_year + 1900,
		             newtime->tm_mon+1, newtime->tm_mday, newtime->tm_hour, newtime->tm_min );

		// list of players
		players[0] = 0;
		if( game.gametype == GAMETYPE_DUEL )
		{
			const char *netname;
			int team;
			edict_t *ent;

			Q_strncatz( players, "_", sizeof( players ) );

			for( team = TEAM_ALPHA; team < TEAM_ALPHA + g_maxteams->integer; team++ )
			{
				if( !teamlist[team].numplayers )
					continue;
				ent = game.edicts + teamlist[team].playerIndices[0];
				netname = COM_RemoveJunkChars( COM_RemoveColorTokens( ent->r.client->pers.netname ) );
				Q_strncatz( players, netname, sizeof( players ) );
				if( team != TEAM_ALPHA + g_maxteams->integer - 1 )
					Q_strncatz( players, "_vs_", sizeof( players ) );
			}
		}

		// combine
		Q_snprintfz( name, sizeof( name ), "%s_%s_%s%s_auto%04i", datetime, GS_Gametype_ShortName( game.gametype ),
		            level.mapname, players, (int)brandom( 0, 9999 ) );

		trap_AddCommandString( va( "serverrecord \"%s\"\n", name ) );
	}
}

//=================
//G_Match_Autorecord_Stats
//=================
void G_Match_Autorecord_Stats( void )
{
	edict_t *ent;

	for( ent = game.edicts + 1; PLAYERNUM( ent ) < game.maxclients; ent++ )
	{
		if( !ent->r.inuse || ent->s.team == TEAM_SPECTATOR || ( ent->r.svflags & SVF_FAKECLIENT ) )
			continue;
		trap_GameCmd( ent, va( "autr stats \"%s\"", G_StatsMessage( ent ) ) );
	}
}

//=================
//G_Match_Autorecord_Stop
//=================
void G_Match_Autorecord_Stop( void )
{
	trap_GameCmd( NULL, "autr stop" );

	if( g_autorecord->integer && game.gametype != GAMETYPE_RACE )
	{
		// stop it
		trap_AddCommandString( "serverrecordstop\n" );

		// check if we wanna delete some
		if( g_autorecord_maxdemos->integer > 0 )
			trap_AddCommandString( va( "serverrecordpurge %i\n", g_autorecord_maxdemos->integer ) );
	}
}

//=================
//G_Match_Autorecord_Cancel
//=================
void G_Match_Autorecord_Cancel( void )
{
	trap_GameCmd( NULL, "autr cancel" );

	if( g_autorecord->integer && game.gametype != GAMETYPE_RACE )
		trap_AddCommandString( "serverrecordcancel\n" );
}

//=================
//G_Match_TimeFinished
//=================
static qboolean G_Match_TimeFinished( void )
{
	if( match.state == MATCH_STATE_NONE )  //always try to activate
		return qtrue;

	if( !match.endtime || level.time < match.endtime )
		return qfalse;

	if( match.state == MATCH_STATE_WARMUP )
		match.forceStart = qtrue; // force match starting when timelimit is up, even if someone goes unready

	if( match.state == MATCH_STATE_WAITEXIT )
	{
		level.exitNow = qtrue;
		return qfalse; // don't advance into next state. The match will be restarted
	}

	return qtrue;
}

//=================
//G_Match_ScorelimitHit
//=================
static qboolean G_Match_ScorelimitHit( void )
{
	edict_t	*e;

	if( match.state != MATCH_STATE_PLAYTIME )
		return qfalse;

	if( game.gametype == GAMETYPE_RACE )
		return qfalse;

	if( g_scorelimit->integer )
	{
		if( !GS_Gametype_IsTeamBased( game.gametype ) )
		{
			for( e = game.edicts+1; PLAYERNUM( e ) < game.maxclients; e++ )
			{
				if( !e->r.inuse )
					continue;

				if( match.scores[PLAYERNUM( e )].score >= g_scorelimit->integer )
					return qtrue;
			}
		}
		else if( game.gametype != GAMETYPE_CA ) // in CA, scorelimit is handled in g_gametype_ca.c: G_Gametype_CA_CheckRules
		{
			int team;

			for( team = TEAM_ALPHA; team < TEAM_ALPHA + g_maxteams->integer; team++ )
			{
				if( teamlist[team].teamscore >= g_scorelimit->integer )
					return qtrue;
			}
		}
	}
	return qfalse;
}

//=================
//G_Match_SuddenDeath
//=================
static qboolean G_Match_SuddenDeath( void )
{
	if( match.state != MATCH_STATE_PLAYTIME )
		return qfalse;

	if( !match.extendedTime || match.endtime )
		return qfalse;

	if( game.gametype == GAMETYPE_RACE )
		return qfalse;

	return !G_Match_Tied();
}

//=================
//G_Match_ScoreAnnouncement
//=================
static qboolean score_announcement_init = qfalse;
static int last_leaders[MAX_CLIENTS];
static int leaders[MAX_CLIENTS];
#define G_ANNOUNCER_READYUP_DELAY 20000; // milliseconds

#if 0
static void G_Match_ResetScoreAnnouncement( void )
{
	score_announcement_init = qfalse;
}
#endif

static qboolean G_IsLeading( edict_t *ent )
{
	int num, i;

	if( GS_Gametype_IsTeamBased( game.gametype ) )
		num = ent->s.team;
	else
		num = PLAYERNUM( ent ) + 1;

	for( i = 0; i < MAX_CLIENTS && leaders[i] != 0; i++ )
	{
		if( leaders[i] == num )
			return qtrue;
	}

	return qfalse;
}

static qboolean G_WasLeading( edict_t *ent )
{
	int num, i;

	if( GS_Gametype_IsTeamBased( game.gametype ) )
		num = ent->s.team;
	else
		num = PLAYERNUM( ent ) + 1;

	for( i = 0; i < MAX_CLIENTS && last_leaders[i] != 0; i++ )
	{
		if( last_leaders[i] == num )
			return qtrue;
	}

	return qfalse;
}

static void G_Match_ScoreAnnouncement( void )
{
	int i;
	edict_t *e, *chased;
	int num_leaders, team;

	if( game.gametype == GAMETYPE_RACE )
		return;

	if( match.state == MATCH_STATE_WARMUP )
	{
		qboolean readyupwarnings = qfalse;
		// ready up announcements
		int START_TEAM, END_TEAM;
		if( GS_Gametype_IsTeamBased( game.gametype ) )
		{
			START_TEAM = TEAM_ALPHA;
			END_TEAM = TEAM_ALPHA + g_maxteams->integer;
		}
		else
		{
			START_TEAM = TEAM_PLAYERS;
			END_TEAM = TEAM_PLAYERS+1;
		}
		for( team = START_TEAM; team < END_TEAM; team++ )
		{
			if( !teamlist[team].numplayers )
				continue;
			for( i = 0; i < teamlist[team].numplayers; i++ )
			{
				e = game.edicts + teamlist[team].playerIndices[i];
				if( e->r.svflags & SVF_FAKECLIENT )
					continue;
				if( match.ready[teamlist[team].playerIndices[i]-1] )
				{
					readyupwarnings = qtrue;
				}
			}
		}

		if( readyupwarnings == qfalse )
			return;

		// now let's repeat and warn
		for( team = START_TEAM; team < END_TEAM; team++ )
		{
			if( !teamlist[team].numplayers )
				continue;
			for( i = 0; i < teamlist[team].numplayers; i++ )
			{
				if( !match.ready[teamlist[team].playerIndices[i]-1] )
				{
					e = game.edicts + teamlist[team].playerIndices[i];
					if( !e->r.client || trap_GetClientState( PLAYERNUM( e ) ) != CS_SPAWNED )
					{
						continue;
					}

					if( e->r.client->pers.readyUpWarningNext < game.realtime )
					{
						e->r.client->pers.readyUpWarningNext = game.realtime + G_ANNOUNCER_READYUP_DELAY;
						e->r.client->pers.readyUpWarningCount++;
						if( e->r.client->pers.readyUpWarningCount > 3 )
						{
							G_AnnouncerSound( e, trap_SoundIndex( S_ANNOUNCER_READY_UP_PISSEDOFF ), GS_MAX_TEAMS, qtrue );
							e->r.client->pers.readyUpWarningCount = 0;
						}
						else
						{
							G_AnnouncerSound( e, trap_SoundIndex( S_ANNOUNCER_READY_UP_POLITE ), GS_MAX_TEAMS, qtrue );
						}
					}
				}
			}
		}
		return;
	}

	if( match.state != MATCH_STATE_PLAYTIME )
		return;

#ifdef DUEL_ARENA
	// we have no good sounds for duel arena
	if( game.gametype == GAMETYPE_DA)
		return;
#endif

	num_leaders = 0;
	memset( leaders, 0, sizeof( leaders ) );

	if( GS_Gametype_IsTeamBased( game.gametype ) )
	{
		int score_max = -999999999;

		for( team = TEAM_ALPHA; team < TEAM_ALPHA + g_maxteams->integer; team++ )
		{
			if( !teamlist[team].numplayers )
				continue;

			if( teamlist[team].teamscore > score_max )
			{
				score_max = teamlist[team].teamscore;
				leaders[0] = team;
				num_leaders = 1;
			}
			else if( teamlist[team].teamscore == score_max )
			{
				leaders[num_leaders++] = team;
			}
		}
		leaders[num_leaders] = 0;
	}
	else
	{
		int score_max = -999999999;

		for( i = 0; i < MAX_CLIENTS && teamlist[TEAM_PLAYERS].playerIndices[i] != -1; i++ )
		{
			if( match.scores[teamlist[TEAM_PLAYERS].playerIndices[i]-1].score > score_max )
			{
				score_max = match.scores[teamlist[TEAM_PLAYERS].playerIndices[i]-1].score;
				leaders[0] = teamlist[TEAM_PLAYERS].playerIndices[i];
				num_leaders = 1;
			}
			else if( match.scores[teamlist[TEAM_PLAYERS].playerIndices[i]-1].score == score_max )
			{
				leaders[num_leaders++] = teamlist[TEAM_PLAYERS].playerIndices[i];
			}
		}
		leaders[num_leaders] = 0;
	}

	if( !score_announcement_init )
	{
		// copy over to last_leaders
		memcpy( last_leaders, leaders, sizeof( leaders ) );
		score_announcement_init = qtrue;
		return;
	}

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

		if( e->r.client->chase.active )
			chased = &game.edicts[e->r.client->chase.target];
		else
			chased = e;

		// floating spectator
		if( chased->s.team == TEAM_SPECTATOR )
		{
			if( !GS_Gametype_IsTeamBased( game.gametype ) )
				continue;

			if( last_leaders[1] == 0 && leaders[1] != 0 )
			{
				G_AnnouncerSound( e, trap_SoundIndex( va( S_ANNOUNCER_SCORE_TEAM_TIED_LEAD_1_to_2, ( rand()&1 )+1 ) ),
				                  GS_MAX_TEAMS, qtrue );
			}
			else if( leaders[1] == 0 && ( last_leaders[0] != leaders[0] || last_leaders[1] != 0 ) )
			{
				G_AnnouncerSound( e, trap_SoundIndex( va( S_ANNOUNCER_SCORE_TEAM_1_to_4_TAKEN_LEAD_1_to_2,
				                                          leaders[0]-1, ( rand()&1 )+1 ) ), GS_MAX_TEAMS, qtrue );
			}
			continue;
		}

		// in the game or chasing someone who is
		if( G_WasLeading( chased ) && !G_IsLeading( chased ) )
		{
			if( GS_Gametype_IsTeamBased( game.gametype ) )
				G_AnnouncerSound( e, trap_SoundIndex( va( S_ANNOUNCER_SCORE_TEAM_LOST_LEAD_1_to_2, ( rand()&1 )+1 ) ),
				                  GS_MAX_TEAMS, qtrue );
			else
				G_AnnouncerSound( e, trap_SoundIndex( va( S_ANNOUNCER_SCORE_LOST_LEAD_1_to_2, ( rand()&1 )+1 ) ),
				                  GS_MAX_TEAMS, qtrue );
		}
		else if( ( !G_WasLeading( chased ) || ( last_leaders[1] != 0 ) ) && G_IsLeading( chased ) && ( leaders[1] == 0 ) )
		{
			if( GS_Gametype_IsTeamBased( game.gametype ) )
				G_AnnouncerSound( e, trap_SoundIndex( va( S_ANNOUNCER_SCORE_TEAM_TAKEN_LEAD_1_to_2, ( rand()&1 )+1 ) ),
				                  GS_MAX_TEAMS, qtrue );
			else
				G_AnnouncerSound( e, trap_SoundIndex( va( S_ANNOUNCER_SCORE_TAKEN_LEAD_1_to_2, ( rand()&1 )+1 ) ),
				                  GS_MAX_TEAMS, qtrue );
		}
		else if( ( !G_WasLeading( chased ) || ( last_leaders[1] == 0 ) ) && G_IsLeading( chased ) && ( leaders[1] != 0 ) )
		{
			if( GS_Gametype_IsTeamBased( game.gametype ) )
				G_AnnouncerSound( e, trap_SoundIndex( va( S_ANNOUNCER_SCORE_TEAM_TIED_LEAD_1_to_2, ( rand()&1 )+1 ) ),
				                  GS_MAX_TEAMS, qtrue );
			else
				G_AnnouncerSound( e, trap_SoundIndex( va( S_ANNOUNCER_SCORE_TIED_LEAD_1_to_2, ( rand()&1 )+1 ) ),
				                  GS_MAX_TEAMS, qtrue );
		}
	}

	// copy over to last_leaders
	memcpy( last_leaders, leaders, sizeof( leaders ) );
}

//=================
//G_Match_SetUpNextState
//=================
void G_Match_SetUpNextState( void )
{
	static qboolean advance_queue = qfalse;
	unsigned int oldstarttime = match.starttime;

	match.state++;
	match.starttime = level.time;
	match.roundstate = MATCH_STATE_NONE;
	match.roundstarttime = level.time;
	match.roundendtime = 0;

	if( match.state == MATCH_STATE_WARMUP )
	{
		advance_queue = qfalse;

		// even if warmup is not enabled, we use it until enough players show up
		if( g_warmup_enabled->integer )
		{
			qboolean enough;

			if( GS_Gametype_IsTeamBased( game.gametype ) )
			{
				int team;

				enough = qtrue;
				for( team = TEAM_ALPHA; team < TEAM_ALPHA + g_maxteams->integer; team++ )
				{
					if( !teamlist[team].numplayers )
						enough = qfalse;
				}
			}
			else
			{
				enough = ( teamlist[TEAM_PLAYERS].numplayers > 1 );
			}

			match.forceStart = qfalse;
			// don't count down warmup time when not enough players
			// endtime changes are done in gametype checkrules when amount of players change
			if( enough && g_warmup_timelimit->value )
				match.endtime = level.time + (int)( fabs( g_warmup_timelimit->value * 60 )*1000 );
			else
				match.endtime = 0;

			//exec the gametype function for setting up the warmup, print warnings, etc
			if( gametypes[game.gametype].warmupMatch )
				gametypes[game.gametype].warmupMatch();
		}
		else
			match.state++;
	}

	if( match.state == MATCH_STATE_COUNTDOWN )
	{
		G_Match_Autorecord_Start();

		if( g_warmup_enabled->integer )
		{
			advance_queue = qtrue;

			match.endtime = level.time + (int)( fabs( g_countdown_time->value )*1000 );

			//exec the gametype function for setting up the warmup, print warnings, etc
			if( gametypes[game.gametype].countdownMatch )
				gametypes[game.gametype].countdownMatch();
		}
		else
			match.state++;
	}

	if( match.state == MATCH_STATE_PLAYTIME )
	{
		advance_queue = qtrue; // shouldn't be needed here

		match.forceStart = qfalse;
		match.extendedTime = 0;
		if( g_timelimit->value )
			match.endtime = level.time + (int)( fabs( 60 * g_timelimit->value )*1000 );
		else
			match.endtime = 0;

		//exec the gametype function for setting up the match, print warnings, etc
		if( gametypes[game.gametype].startMatch )
			gametypes[game.gametype].startMatch();
	}

	if( match.state == MATCH_STATE_POSTMATCH )
	{
		match.endtime = level.time + fabs( G_POSTMATCH_TIMELIMIT ); // postmatch time in seconds

		G_Timeout_Reset();
		game.lock_teams = qfalse;

		//exec the gametype function for setting up the postmatch, print warnings, etc
		if( gametypes[game.gametype].endMatch )
			gametypes[game.gametype].endMatch();

		// if we were reverted to playtime, it's extended time
		if( match.state == MATCH_STATE_PLAYTIME )
		{
			match.starttime = oldstarttime;
			match.extendedTime += 1;
		}
		else
		{
			G_Match_Autorecord_Stats();
		}
		match.forceExit = qfalse;
	}

	if( match.state == MATCH_STATE_WAITEXIT )
	{
		G_Match_Autorecord_Stop();
		if( advance_queue )
		{
			G_Teams_AdvanceChallengersQueue();
			advance_queue = qtrue;
		}

		match.endtime = level.time + fabs( 25000 ); //maximum time to wait before changing map in milliseconds
		level.exitNow = qfalse;
	}

	if( match.state < MATCH_STATE_NONE || match.state > MATCH_STATE_WAITEXIT )
	{
		//if it wasn't a known game state reboot the machine
		match.state = MATCH_STATE_NONE;
		match.endtime = 0;
		match.roundstate = MATCH_STATE_NONE;
		match.roundendtime = 0;
	}

	G_UpdatePlayersMatchMsgs();
}

//=================
//G_EndMatch
//=================
void G_EndMatch( void )
{
	match.forceExit = qtrue;
	match.state = MATCH_STATE_POSTMATCH - 1;
	G_Match_SetUpNextState();
}

//=================
//G_Match_CheckReadys
//=================
void G_Match_CheckReadys( void )
{
	edict_t *e;
	qboolean allready;
	int readys, notreadys, teamsready;
	int team, i;

	if( !g_warmup_enabled->integer )
		return;

	if( match.state != MATCH_STATE_WARMUP && match.state != MATCH_STATE_COUNTDOWN )
		return;

	if( match.state == MATCH_STATE_COUNTDOWN && match.forceStart )
		return; // never stop countdown if we have run out of warmup_timelimit

	teamsready = 0;
	for( team = TEAM_PLAYERS; team < TEAM_ALPHA + g_maxteams->integer; team++ )
	{
		readys = notreadys = 0;
		for( i = 0; teamlist[team].playerIndices[i] != -1; i++ )
		{
			e = game.edicts + teamlist[team].playerIndices[i];

			if( !e->r.inuse )
				continue;
			if( e->s.team == TEAM_SPECTATOR )  //ignore spectators
				continue;

			if( match.ready[PLAYERNUM( e )] )
				readys++;
			else
				notreadys++;
		}
		if( !notreadys && readys )
			teamsready++;
	}

	// everyone has commited
	if( GS_Gametype_IsTeamBased( game.gametype ) )
	{
		if( teamsready == g_maxteams->integer )
			allready = qtrue;
		else
			allready = qfalse;
	}
	else
	{       //ffa
		if( teamsready && teamlist[TEAM_PLAYERS].numplayers > 1 )
			allready = qtrue;
		else
			allready = qfalse;
	}

	if( allready == qtrue && match.state != MATCH_STATE_COUNTDOWN )
	{
		G_PrintMsg( NULL, "All players are ready.  Match starting!\n" );
		G_Match_SetUpNextState();
	}
	else if( allready == qfalse && match.state == MATCH_STATE_COUNTDOWN )
	{
		G_PrintMsg( NULL, "Countdown aborted.\n" );
		G_CenterPrintMsg( NULL, "COUNTDOWN ABORTED\n" );
		G_Match_Autorecord_Cancel();
		match.state = MATCH_STATE_NONE;
		match.endtime = 0;
		match.roundstate = MATCH_STATE_NONE;
		match.roundendtime = 0;
	}
}

//=================
//G_Match_Ready
//=================
void G_Match_Ready( edict_t *ent )
{
	if( ent->r.svflags & SVF_FAKECLIENT && match.ready[PLAYERNUM( ent )] == qtrue )
		return;

	if( ent->s.team == TEAM_SPECTATOR )
	{
		G_PrintMsg( ent, "Join the game first\n" );
		return;
	}

	if( match.state != MATCH_STATE_WARMUP )
	{
		if( !( ent->r.svflags & SVF_FAKECLIENT ) )
			G_PrintMsg( ent, "We're not in warmup.\n" );
		return;
	}

	if( match.ready[PLAYERNUM( ent )] )
	{
		G_PrintMsg( ent, "You are already ready.\n" );
		return;
	}

	match.ready[PLAYERNUM( ent )] = qtrue;
	G_PrintMsg( NULL, "%s%s is ready!\n", ent->r.client->pers.netname, S_COLOR_WHITE );
	G_UpdatePlayerMatchMsg( ent );

	G_Match_CheckReadys();
}

//=================
//G_Match_NotReady
//=================
void G_Match_NotReady( edict_t *ent )
{
	if( ent->s.team == TEAM_SPECTATOR )
	{
		G_PrintMsg( ent, "Join the game first\n" );
		return;
	}

	if( match.state != MATCH_STATE_WARMUP && match.state != MATCH_STATE_COUNTDOWN )
	{
		G_PrintMsg( ent, "A match is not being setup.\n" );
		return;
	}

	if( !match.ready[PLAYERNUM( ent )] )
	{
		G_PrintMsg( ent, "You weren't ready.\n" );
		return;
	}

	match.ready[PLAYERNUM( ent )] = qfalse;
	G_PrintMsg( NULL, "%s%s is no longer ready.\n", ent->r.client->pers.netname, S_COLOR_WHITE );
	G_UpdatePlayerMatchMsg( ent );

	G_Match_CheckReadys();
}

//=================
//G_Match_ToggleReady
//=================
void G_Match_ToggleReady( edict_t *ent )
{
	if( !match.ready[PLAYERNUM( ent )] )
		G_Match_Ready( ent );
	else
		G_Match_NotReady( ent );
}


//==========================================================
//	Reset the Match
//==========================================================

//=================
//G_Match_RespawnAllItems
//Note: respawns the items right this frame
//=================
void G_Match_RespawnAllItems( void )
{
	edict_t *ent;

	//JALFIXME: I believe powerups and megahealth need special treatment
	for( ent = game.edicts + game.maxclients; ENTNUM( ent ) < game.numentities; ent++ )
	{
		if( ent->r.inuse && !ent->r.client && ent->r.solid == SOLID_NOT && ent->nextthink >= level.time )
		{
			if( ent->think == DoRespawn || ent->think == MegaHealth_think )
			{
				if( !( ent->spawnflags & DROPPED_ITEM ) && !( ent->spawnflags & DROPPED_PLAYER_ITEM ) &&
				   G_Gametype_CanRespawnItem( ent->item ) )
				{
					SetRespawn( ent, FRAMETIME );
				}
				else
				{
					G_FreeEdict( ent ); //spawn or free
				}
			}
		}
		else
		{
			if( ent->spawnflags & DROPPED_ITEM || ent->spawnflags & DROPPED_PLAYER_ITEM )
				G_FreeEdict( ent );
		}
	}
}

// synced powerupdelay
static float powerup_respawn_delay = 0.0f;

//=================
//G_Gametype_ItemRespawnDelay
//=================
float G_Gametype_ItemRespawnDelay( gitem_t *item )
{
	switch( item->type )
	{
	case IT_ARMOR:
		if( game.gametype == GAMETYPE_DUEL )
			return 15.0f;
		else
			return 0.0f;

	case IT_HEALTH:
		if( !Q_stricmp( "Mega Health", item->pickup_name ) && game.gametype == GAMETYPE_DUEL )
			return 15.0f;
		else
			return 0.0f;

	case IT_POWERUP:
		return powerup_respawn_delay;

		// fall through
	case IT_WEAPON:
	case IT_AMMO:
	case IT_FLAG:
	default:
		return 0.0f;
	}
}

//=================
//G_Match_SetUpDelayedItems
//=================
void G_Match_SetUpDelayedItems( void )
{
	edict_t *ent;

	powerup_respawn_delay = brandom( 15.0f, 30.0f ); // generate same spawn delay for all powerups

	//JALFIXME: I believe powerups and megahealth need special treatment
	for( ent = game.edicts + game.maxclients; ENTNUM( ent ) < game.numentities; ent++ )
	{
		if( ent->item && !( ent->spawnflags & DROPPED_ITEM ) && !( ent->spawnflags & DROPPED_PLAYER_ITEM ) )
		{
			if( G_Gametype_CanRespawnItem( ent->item ) && G_Gametype_ItemRespawnDelay( ent->item ) )
				SetRespawn( ent, G_Gametype_ItemRespawnDelay( ent->item ) );
		}
	}
}

//=================
//G_Match_RemoveAllClientLasers
// Vic: FIXME, this is incredibly stupid
//=================
void G_Match_RemoveAllClientLasers( void )
{
	edict_t *ent;

	for( ent = game.edicts + game.maxclients; ENTNUM( ent ) < game.numentities; ent++ )
	{
		if( ent->s.weapon == WEAP_LASERGUN )
		{
			G_HideClientLaser( ent );
		}
	}
}

//=================
//G_Match_RemoveAllProjectiles
//=================
void G_Match_RemoveAllProjectiles( void )
{
	edict_t *ent;

	for( ent = game.edicts + game.maxclients; ENTNUM( ent ) < game.numentities; ent++ )
	{
		if( ent->r.inuse && !ent->r.client && ent->r.svflags & SVF_PROJECTILE && ent->r.solid != SOLID_NOT )
		{
			G_FreeEdict( ent );
		}
	}
}

//=================
//G_Match_RespawnAllEntities
//=================
static void G_Match_RespawnAllEntities( void )
{
	G_Gametype_Update();

	G_SpawnMapEntities( qfalse );
}

//=================
//G_Match_FreeBodyQueue
//=================
void G_Match_FreeBodyQueue( void )
{
	edict_t *ent;
	int i;

	ent = &game.edicts[game.maxclients + 1];
	for( i = 0; i < BODY_QUEUE_SIZE; ent++, i++ )
	{
		if( !ent->r.inuse )
			continue;

		if( ent->classname && !Q_stricmp( ent->classname, "body" ) )
		{
			GClip_UnlinkEntity( ent );

			ent->deadflag = DEAD_NO;
			ent->movetype = MOVETYPE_NONE;
			ent->r.solid = SOLID_NOT;
			ent->r.svflags = SVF_NOCLIENT;

			ent->s.type = ET_GENERIC;
			ent->s.skinnum = 0;
			ent->s.frame = 0;
			ent->s.modelindex = 0;
			ent->s.sound = 0;
			ent->s.effects = 0;
			ent->s.renderfx = 0;

			ent->takedamage = DAMAGE_NO;
			ent->flags |= FL_NO_KNOCKBACK;

			GClip_LinkEntity( ent );
		}
	}

	level.body_que = 0;
}

//=================
//G_Match_RespawnAllClients
//Note: respawn all clients *right now*
//Note2: It also resets their spawncount value.
//=================
void G_Match_RespawnAllClients( void )
{
	edict_t *ent;
	edict_t *alpha_coach;
	edict_t *beta_coach;

	for( ent = game.edicts + 1; PLAYERNUM( ent ) < game.maxclients; ent++ )
	{
		if( ent->r.inuse && ent->r.solid != SOLID_NOT )
		{
			ent->r.solid = SOLID_NOT;
			GClip_UnlinkEntity( ent );
		}
	}

	alpha_coach = NULL;
	beta_coach = NULL;

	for( ent = game.edicts + 1; PLAYERNUM( ent ) < game.maxclients; ent++ )
	{
		if( !ent->r.inuse )
			continue;

		if( ent->s.team == TEAM_SPECTATOR )  //ignore spectators
			continue;
		
		// create a pointer to the coach(es) so we can give them a chase target once teammates have spawned
		if( ent->r.client->is_coach && GS_Gametype_IsTeamBased( game.gametype ) )  
		{
			if( ent->s.team == TEAM_ALPHA )
				alpha_coach = ent;
			else if ( ent->s.team == TEAM_BETA )
				beta_coach = ent;
			continue;
		}

		if( ent->s.weapon == WEAP_LASERGUN )
			G_HideClientLaser( ent );

		//reset respawn count
		ent->r.client->resp.respawnCount = 0;

		//powerup info
		ent->r.client->quad_timeout = 0;
		ent->r.client->shell_timeout = 0;

		//spawn
		G_Gametype_ClientRespawn( ent );
	}
	// give each coach found a chase target now
	if( alpha_coach != NULL )
		G_ChasePlayer( alpha_coach, NULL, qtrue, 0 );
	if ( beta_coach != NULL )
		G_ChasePlayer( beta_coach, NULL, qtrue, 0 );
}


//=================
//G_Match_NewMap
//=================
void G_Match_NewMap( void )
{
	match.state = MATCH_STATE_NONE;
	match.endtime = 0;
	match.roundstate = MATCH_STATE_NONE;
	match.roundendtime = 0;

	//unlock teams
	G_Teams_NewMap();
	G_CallVotes_Reset();
	G_Timeout_Reset();

	//unready all clients
	memset( match.ready, qfalse, sizeof( match.ready ) );

	//call gametype specific
	if( gametypes[game.gametype].newMap )
		gametypes[game.gametype].newMap();

	AI_NewMap(); //MbotGame

	// update server rules configstring
	G_ServerSettings_ConfigString();

	G_SetMMReadyState();
}


//=================
//G_Match_RestartLevel
//=================
qboolean G_Match_RestartLevel( void )
{
	G_Match_FreeBodyQueue();
	G_Match_RespawnAllEntities();
	G_Match_NewMap();

	return qtrue;
}

//=================
//G_MoveClientToPostMatchScoreBoards
//=================
void G_MoveClientToPostMatchScoreBoards( edict_t *ent, edict_t *spawnpoint )
{
	gclient_t *client = ent->r.client;

	if( !spawnpoint )
		spawnpoint = world;

	VectorCopy( spawnpoint->s.origin, ent->s.origin );
	VectorCopy( spawnpoint->s.origin, client->ps.pmove.origin );
	VectorCopy( spawnpoint->s.angles, client->ps.viewangles );
	VectorClear( ent->velocity );
	client->ps.pmove.pm_type = PM_FREEZE;
	client->ps.pmove.pm_flags |= PMF_NO_PREDICTION;
	client->chase.active = qfalse;
	G_Gametype_CTF_ResetClientFlag( ent );

	// clear inventory
	memset( client->inventory, 0, sizeof( client->inventory ) );
	client->latched_weapon = WEAP_NONE;
	client->selected_item = -1;

	// clean up powerup info
	client->weapon_sound = 0;
	client->quad_timeout = client->shell_timeout = 0;
	memset( &client->weaponstate, 0, sizeof( client->weaponstate ) );

	ent->viewheight = 0;
	ent->s.modelindex = ent->s.modelindex2 = 0;
	ent->s.effects = 0;
	ent->s.weapon = 0;
	ent->s.sound = 0;
	ent->s.light = 0;
	ent->r.solid = SOLID_NOT;
	ent->takedamage = DAMAGE_NO;
	ent->movetype = MOVETYPE_NONE;
}

//=================
//G_GameType_ConfigString
//=================
void G_ServerSettings_ConfigString( void )
{
	char configstring[MAX_CONFIGSTRING_CHARS];

	Q_snprintfz( configstring, sizeof( configstring ), "0 %i %i %i %i %i",
	             G_Gametype_hasChallengersQueue( game.gametype ),
	             g_maxteams->integer,
	             game.gametype,
	             ( g_instagib->integer != 0 ),
				 ( g_allow_falldamage->integer != 0 )
	);

	trap_ConfigString( CS_SERVERSETTINGS, configstring );
}

//=================
//G_GameType_BeginPostMatch
//=================
void G_GameType_BeginPostMatch( void )
{
	edict_t	*spawnpoint, *e;

	// respawn any dead clients
	for( e = game.edicts + 1; PLAYERNUM( e ) < game.maxclients; e++ )
	{
		if( !e->r.inuse )
			continue;
		if( G_IsDead( e ) )
		{
			G_Gametype_ClientRespawn( e );
		}
		else if( e->s.weapon == WEAP_LASERGUN )
		{
			G_HideClientLaser( e );
		}
	}

	spawnpoint = G_SelectIntermissionSpawnPoint();

	// move all clients to the intermission point
	for( e = game.edicts + 1; PLAYERNUM( e ) < game.maxclients; e++ )
	{
		if( !e->r.inuse )
			continue;
		G_MoveClientToPostMatchScoreBoards( e, spawnpoint );
	}
}

// returns true once each second
qboolean G_Match_GenericCountDownAnnounces( void )
{
	static int lastsecond;
	static float remainingtime;
	static int remainingseconds;

	if( match.state >= MATCH_STATE_POSTMATCH )
		return qfalse;

	if( !match.endtime )
		return qfalse;

	remainingtime = (float)( match.endtime - level.time ) * 0.001f;
	remainingseconds = (int)remainingtime;

	//do the next operations only once per second
	if( lastsecond == remainingseconds )
		return qfalse;

	lastsecond = remainingseconds;

	//print some countdowns
	if( match.state == MATCH_STATE_COUNTDOWN )
	{
		if( 1 + remainingseconds <= g_countdown_time->integer )
		{
			//countdown sound
			if( 1 + remainingseconds < 4 )
				G_AnnouncerSound( NULL, trap_SoundIndex( va( S_ANNOUNCER_COUNTDOWN_COUNT_1_to_3_SET_1_to_2,
				                                             1 + remainingseconds, 1 ) ), GS_MAX_TEAMS, qfalse ); // todo: use random countdown set for all three numbers
			G_CenterPrintMsg( NULL, "%i\n", 1 + remainingseconds );
		}
	}
	else if( match.state == MATCH_STATE_PLAYTIME )
	{
		if( ( 1 + remainingseconds <= g_countdown_time->integer ) && g_timelimit->integer )
		{
			G_CenterPrintMsg( NULL, "%i\n", 1 + remainingseconds );
		}
	}

	return qtrue;
}


//======================================================
//		Game types
//======================================================


//=================
//G_Gametype_Init
//
// Note: g_gametype cvar is only loaded inside this
// function to update game.gametype. Gametype can only
// be changed from the server config or callvote.
// This is for coding sanity (so we don't make g_gametype->integer checks).
//=================
void G_Gametype_Init( void )
{
	int type;

	// first of all, verify that all gametypes have a struct
	if( GAMETYPE_TOTAL >= ( sizeof( gametypes )/sizeof( gametype_t ) ) )
		G_Error( "G_Gametype_Init: Failed to initialize gametypes. Missing gametype definition\n" );

	for( type = GAMETYPE_DM; type < GAMETYPE_TOTAL; type++ )
	{
		if( GS_Gametype_ShortName( type ) == NULL )
		{
			G_Error( "G_Gametype_Init: Failed to initialize gametypes. Gametype %i didn't return a shortname\n", type );
		}
	}

	// the gametype cvar is only read at initialization
	g_gametype =
	        trap_Cvar_Get( "g_gametype", GS_Gametype_ShortName( GAMETYPE_DM ), CVAR_SERVERINFO|CVAR_ARCHIVE|CVAR_LATCH );
	game.gametype = GS_Gametype_FindByShortName( g_gametype->string );
	if( game.gametype < GAMETYPE_DM || game.gametype >= GAMETYPE_TOTAL )
	{
		G_Printf( "G_Gametype: Wrong value. Setting up with default (DeathMatch)\n" );
		game.gametype = GAMETYPE_DM;
		trap_Cvar_Set( "g_gametype", GS_Gametype_ShortName( game.gametype ) );
	}

	// empty string to allow all
	g_votable_gametypes =	trap_Cvar_Get( "g_votable_gametypes", "", CVAR_ARCHIVE );

	//get the match cvars too
	g_warmup_enabled =	trap_Cvar_Get( "g_warmup_enabled", "1", CVAR_ARCHIVE );
	g_warmup_timelimit =	trap_Cvar_Get( "g_warmup_timelimit", "5", CVAR_ARCHIVE );
	g_countdown_time =	trap_Cvar_Get( "g_countdown_time", "5", CVAR_ARCHIVE );
	g_match_extendedtime =	trap_Cvar_Get( "g_match_extendedtime", "2", CVAR_ARCHIVE );

	// game settings
	g_timelimit =		trap_Cvar_Get( "g_timelimit", "10", CVAR_ARCHIVE );
	g_scorelimit =		trap_Cvar_Get( "g_scorelimit", "0", CVAR_ARCHIVE );
	g_allow_falldamage =	trap_Cvar_Get( "g_allow_falldamage", "1", CVAR_ARCHIVE );

	//init teams
	G_Teams_Init();

	//lauch each gametype specific Initialization
	if( gametypes[game.gametype].initGametype )
		gametypes[game.gametype].initGametype();
}

void G_Gametype_Update( void )
{
	edict_t	*ent;

	if( !g_gametype->latched_string )
		return;

	//put all clients as spectators
	for( ent = game.edicts + 1; PLAYERNUM( ent ) < game.maxclients; ent++ )
	{
		if( !ent->r.inuse || !ent->r.client || trap_GetClientState( PLAYERNUM( ent ) ) < CS_SPAWNED )
			continue;
		G_Teams_SetTeam( ent, TEAM_SPECTATOR ); // it also cleans up scores, respawncount, etc.
		ent->r.client->pers.queueTimeStamp = 0;
	}

	trap_Cvar_ForceSet( "g_gametype", va( "%s", g_gametype->latched_string ) );
	game.gametype = GS_Gametype_FindByShortName( g_gametype->string );
	if( game.gametype < GAMETYPE_DM || game.gametype >= GAMETYPE_TOTAL )
	{
		G_Printf( "G_Gametype: Wrong value. Setting up with default (dm)\n" );
		game.gametype = GAMETYPE_DM;
		trap_Cvar_Set( "g_gametype", GS_Gametype_ShortName( game.gametype ) );
	}

	//lauch each gametype specific Initialization
	if( gametypes[game.gametype].initGametype )
		gametypes[game.gametype].initGametype();

	// update server rules configstring
	G_ServerSettings_ConfigString();
}

qboolean G_Gametype_IsVotable( int type )
{
	char *ptr = g_votable_gametypes->string;
	char *validname;

	// if the votable gametypes list is empty, allow all but SP
	if( ptr == NULL || ptr[0] == 0 )
	{
		return qtrue;
	}

	// check for the gametype being in the votable gametypes list
	while( ptr && *ptr )
	{
		validname = COM_Parse( &ptr );
		if( !validname[0] )
			break;

		if( GS_Gametype_FindByShortName( validname ) == type )
			return qtrue;
	}

	return qfalse;
}

//=================
//G_Gametype_hasChallengersQueue
//=================
qboolean G_Gametype_hasChallengersQueue( int type )
{
	if( !g_challengers_queue->integer )
		return qfalse;

	return ( gametypes[game.gametype].hasChallengersQueue != 0 );
}

//=================
//G_GameType_ClientHealthRule
//=================
void G_GameType_ClientHealthRule( void )
{
	edict_t	*e;

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

		if( e->health > e->max_health && !gtimeout.active )
		{
			e->health -= game.snapFrameTime * 0.001f;
			if( e->health < e->max_health )
				e->health = e->max_health;
		}
	}
}

//=================
//G_GameType_ClientArmorDecayRule
//=================
void G_GameType_ClientArmorDecayRule( void )
{
	edict_t	*e;

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

		if( e->r.client->armor > 150 && !gtimeout.active )
		{
			e->r.client->armor -= game.snapFrameTime * 0.001f;
			if( e->r.client->armor < 150 )
				e->r.client->armor = 150;
		}
	}
}

//=================
//G_Gametype_ShortName
//=================
char *G_Gametype_ScoreboardMessage( void )
{
	if( gametypes[game.gametype].scoreboardMessage )
		return gametypes[game.gametype].scoreboardMessage();

	return NULL;
}

//=================
//G_Gametypes_ClienBegin
// the client just finished connecting to the server
//=================
void G_Gametypes_ClienBegin( edict_t *ent )
{
	if( gametypes[game.gametype].clientbegin )
		gametypes[game.gametype].clientbegin( ent );
	else
		ClientBeginMultiplayerGame( ent ); //prevent undefinitions
}

//=================
//G_Gametype_Killed
//some entity was killed. Count the score if needed
//returning true stops the generic function to continue (everything should be handled here).
//=================
qboolean G_Gametype_Killed( edict_t *targ, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point, int mod )
{
	int contents;

	if( targ->r.svflags & SVF_MONSTER )
		return qfalse; //handle in generic func

	// doors, triggers, etc
	if( targ->movetype == MOVETYPE_PUSH || targ->movetype == MOVETYPE_STOP || targ->movetype == MOVETYPE_NONE )
		return qfalse; // handle in generic func

	contents = G_PointContents( targ->s.origin );

	//clients / bots: add the frag to scores
	if( attacker && match.state == MATCH_STATE_PLAYTIME && !( targ->r.svflags & SVF_CORPSE ) )
	{
		if( game.gametype == GAMETYPE_CTF )
		{
			G_Gametype_CTF_FragBonuses( targ, inflictor, attacker, mod );
			// drop items
			if( targ->r.client && !( contents & CONTENTS_NODROP ) )
			{
				G_DropClientBackPack( targ );
				TossClientWeapon( targ );
			}
		}
		else if( game.gametype == GAMETYPE_TDM )
		{
			G_Gametype_TDM_FragBonuses( targ, inflictor, attacker, mod );
			// drop items
			if( targ->r.client && !( contents & CONTENTS_NODROP ) )
			{
				G_DropClientBackPack( targ );
				TossClientWeapon( targ );
			}
		}
		else if( game.gametype == GAMETYPE_CA )
		{
			G_Gametype_CA_FragBonuses( targ, inflictor, attacker, mod );
		}
#ifdef DUEL_ARENA
		else if( game.gametype == GAMETYPE_DA )
		{
			G_Gametype_DA_FragBonuses( targ, inflictor, attacker, mod );
		}
#endif
		else //GAMETYPE_DM || GAMETYPE_DUEL
		{
			// killed by the world
			if( attacker == world && targ->r.client )
			{
				if( mod == MOD_FALLING )  //should have cratereds++
					match.scores[PLAYERNUM( targ )].suicides++;

				match.scores[PLAYERNUM( targ )].deaths++;
				match.scores[PLAYERNUM( targ )].score--;

				if( game.gametype == GAMETYPE_DUEL )
				{
					teamlist[targ->s.team].teamplayerscores--;
					teamlist[targ->s.team].teamscore--;
				}

			}
			else if( attacker == targ )
			{                  //self fragged

				match.scores[PLAYERNUM( attacker )].score--;
				match.scores[PLAYERNUM( attacker )].deaths++;
				match.scores[PLAYERNUM( attacker )].suicides++;

				if( game.gametype == GAMETYPE_DUEL )
				{
					teamlist[attacker->s.team].teamplayerscores--;
					teamlist[attacker->s.team].teamscore--;
				}

			}
			else
			{

				match.scores[PLAYERNUM( attacker )].score++;
				match.scores[PLAYERNUM( attacker )].kills++;
				if( targ->r.client )
					match.scores[PLAYERNUM( targ )].deaths++;

				if( game.gametype == GAMETYPE_DUEL )
				{
					teamlist[attacker->s.team].teamplayerscores++;
					teamlist[attacker->s.team].teamscore++;
				}
			}

			//drop items
			if( targ->r.client && !( contents & CONTENTS_NODROP ) )
			{
				G_DropClientBackPack( targ );
				TossClientWeapon( targ );
			}
		}
	}

	// the race is not really in playing state
	if( game.gametype == GAMETYPE_RACE )
	{
		// we died while in the race
		if( targ->r.client && targ->r.client->resp.race_active )
		{
			// reset flag
			targ->r.client->resp.race_active = qfalse;
			targ->r.client->resp.race_time = 0;
		}
	}

	targ->die( targ, inflictor, attacker, damage, point );
	return qtrue;
}

//=================
//G_Gametype_CanPickUpItem
//=================
qboolean G_Gametype_CanPickUpItem( gitem_t *item )
{
	if( !item )
		return qfalse;

	return ( item->type & match.pickableItemsMask );
}

//=================
//G_Gametype_CanSpawnItem
//=================
qboolean G_Gametype_CanSpawnItem( gitem_t *item )
{
	int itemmask;
	int i;
	char *captureitem;

	if( !item )
		return qfalse;

	itemmask = GS_Gametype_SpawnableItemMask( game.gametype );
	if( g_instagib->integer )
	{
		itemmask &= ~G_INSTAGIB_NEGATE_ITEMMASK;

		// if item is needed for iTDM functionality, keep it
		if( game.gametype == GAMETYPE_TDM )
		{
			if( !Q_stricmp( item->classname, "trigger_capture_area" ) )
				return qtrue;

			for( i = 0, captureitem = capture_items[0]; captureitem; captureitem = capture_items[++i] )
			{
				if( !Q_stricmp( item->classname, captureitem ) )
					return qtrue;
			}

			// we use ctf flag as a capture area as well
			itemmask |= IT_FLAG;
		}
	}

	return ( itemmask & item->type );
}

//=================
//G_Gametype_CanRespawnItem
//=================
qboolean G_Gametype_CanRespawnItem( gitem_t *item )
{
	int itemmask;
	if( !item )
		return qfalse;

	itemmask = GS_Gametype_RespawnableItemMask( game.gametype );
	if( g_instagib->integer )
	{
		itemmask &= ~G_INSTAGIB_NEGATE_ITEMMASK;
	}

	return ( itemmask & item->type );
}

//=================
//G_Gametype_CanDropItem
//=================
qboolean G_Gametype_CanDropItem( gitem_t *item, qboolean ignoreMatchState )
{
	int itemmask;

	if( !item )
		return qfalse;

	if( !ignoreMatchState )
	{
		if( match.state != MATCH_STATE_PLAYTIME && match.state != MATCH_STATE_WARMUP )
			return qfalse;
	}

	itemmask = GS_Gametype_DropableItemMask( game.gametype );
	if( g_instagib->integer )
	{
		itemmask &= ~G_INSTAGIB_NEGATE_ITEMMASK;
	}

	return ( itemmask & item->type );
}

//=================
//G_Gametype_CanFallDamage
//=================
qboolean G_Gametype_CanFallDamage( void )
{
	return g_allow_falldamage->integer;
}

//=================
//G_Gametype_CanTeamDamage
//=================
qboolean G_Gametype_CanTeamDamage( int damageflags )
{
	if( damageflags & DAMAGE_NO_PROTECTION )
		return qtrue;

	if( !GS_Gametype_IsTeamBased( game.gametype ) )
		return qtrue;

	// ca team damage is a exception
	if( game.gametype == GAMETYPE_CA )
		return ( g_ca_allow_teamdamage->integer != 0 );

	return g_teams_teamdamage->integer;
}

//=================
//G_Gametype_CanRespawnItem
//=================
float G_Gametype_RespawnTimeForItem( gitem_t *item )
{
	if( !item )
		return -1; //free the edict

	if( item->type & IT_AMMO )
	{
		if( g_ammo_respawn->integer > 0 )
			return g_ammo_respawn->integer;

		return gametypes[game.gametype].ammo_respawn;
	}

	if( item->type & IT_WEAPON )
	{
		if( g_weapon_respawn->integer > 0 )
			return g_weapon_respawn->integer;

		return gametypes[game.gametype].weapon_respawn;
	}

	if( item->tag == HEALTH_MEGA )
	{
		return gametypes[game.gametype].megahealth_respawn;
	}

	if( item->type & IT_HEALTH )
	{
		if( g_health_respawn->integer > 0 )
			return g_health_respawn->integer;

		return gametypes[game.gametype].health_respawn;
	}

	if( item->type & IT_ARMOR )
	{
		if( g_armor_respawn->integer > 0 )
			return g_armor_respawn->integer;

		return gametypes[game.gametype].armor_respawn;
	}

	if( item->type & IT_POWERUP )
	{
		if( item->tag == POWERUP_SHELL )
			return gametypes[game.gametype].powerup_respawn * 2;
		else
			return gametypes[game.gametype].powerup_respawn;
	}

	return item->quantity;
}

int G_Gametype_SpawnPointRadius( void )
{
	if( g_instagib->integer )
		return gametypes[game.gametype].spawnpoint_radius * 2;

	return gametypes[game.gametype].spawnpoint_radius;
}

//=================
//G_Gametype_DroppedItemTimeout
// the time before items free themselves after being dropped (0 = never)
//=================
int G_Gametype_DroppedItemTimeout( gitem_t *item )
{
	// to do: add cvar
	return 29;
}

//=================
//G_Gametype_ClientRespawn
//=================
qboolean G_Gametype_ClientRespawn( edict_t *self )
{
	self->r.client->respawn_timestamp = level.time;

	// always clear the entity snap info when respawning
	memset( &self->snap, 0, sizeof( self->snap ) );

	if( gametypes[game.gametype].respawn )
	{
		qboolean respawned = gametypes[game.gametype].respawn( self );
		self->r.client->resp.respawnCount++;
		return respawned;
	}

	return qfalse;
}

static qboolean G_EachNewSecond( void )
{
	static int lastsecond;
	static int second;

	second = (int)( level.time*0.001 );
	if( lastsecond == second )
		return qfalse;

	lastsecond = second;
	return qtrue;
}
static void G_CheckNumBots( void )
{
	edict_t	*ent;

	// check sanity of g_numbots
	if( g_numbots->integer < 0 )
		trap_Cvar_Set( "g_numbots", "0" );

	if( g_numbots->integer > game.maxclients )
		trap_Cvar_Set( "g_numbots", va( "%i", game.maxclients ) );

	if( g_numbots->integer < game.numBots )
	{                                     // kick one bot
		for( ent = game.edicts + game.maxclients; PLAYERNUM( ent ) >= 0; ent-- )
		{
			if( !ent->r.inuse || !( ent->r.svflags & SVF_FAKECLIENT ) )
				continue;
			if( ent->ai.type == AI_ISBOT )
			{
				trap_DropClient( ent, DROP_TYPE_GENERAL, NULL );
				break;
			}
		}
		return;
	}

	if( g_numbots->integer > game.numBots )
	{                                     // add a bot if there is room
		for( ent = game.edicts + 1; PLAYERNUM( ent ) < game.maxclients && game.numBots < g_numbots->integer; ent++ )
		{
			if( !ent->r.inuse && trap_GetClientState( PLAYERNUM( ent ) ) == CS_FREE )
				BOT_SpawnBot( NULL );
		}
	}

}

static qboolean G_EachNewMinute( void )
{
	static int lastminute;
	static int minute;

	minute = (int)( level.time*0.001 / 60.0f );
	if( lastminute == minute )
		return qfalse;

	lastminute = minute;
	return qtrue;
}
static void G_CheckEvenTeam( void )
{
	int max = 0;
	int min = game.maxclients + 1;
	int uneven_team = TEAM_SPECTATOR;
	int i;

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

	if( g_teams_allow_uneven->integer )
		return;

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

	if( max - min > 1 )
	{
		for( i = 0; teamlist[uneven_team].playerIndices[i] != -1; i++ )
		{
			edict_t	*e = game.edicts + teamlist[uneven_team].playerIndices[i];
			if( !e->r.inuse )
				continue;
			G_CenterPrintMsg( e, "Teams are uneven. Please switch into another team.\n" ); // FIXME: need more suitable message :P
			G_PrintMsg( e, "%sTeams are uneven. Please switch into another team.\n", S_COLOR_CYAN ); // FIXME: need more suitable message :P
		}
		// FIXME: switch team forcibly?
	}
}

//=================
//G_GametypeCheckRules
//=================
void G_GametypeCheckRules( void )
{
	G_UpdateScoreBoardMessages();
	G_Teams_ExecuteChallengersQueue();
	G_Teams_UpdateMembersList();
	G_CallVotes_Think();

	//fixme: should scorelimit be inside each gametype rules?
	if( G_Match_ScorelimitHit() )
	{
		G_PrintMsg( NULL, "Scorelimit hit.\n" );
		G_Match_SetUpNextState();
		return;
	}

	if( G_Match_TimeFinished() )
	{
		if( match.state == MATCH_STATE_PLAYTIME && game.gametype != GAMETYPE_CA )
			G_PrintMsg( NULL, "Timelimit hit.\n" );
		G_Match_SetUpNextState();
		return;
	}

	if( G_Match_SuddenDeath() )
	{
		G_Match_SetUpNextState();
		return;
	}

	G_Match_ScoreAnnouncement();

	// abort match / toggle warmup timelimit based on whether there are enough players in the game
	if( game.gametype != GAMETYPE_RACE && g_warmup_enabled->integer )
	{
		qboolean any = qfalse;
		qboolean enough;

		if( GS_Gametype_IsTeamBased( game.gametype ) )
		{
			int team, emptyteams = 0;

			for( team = TEAM_ALPHA; team < TEAM_ALPHA + g_maxteams->integer; team++ )
			{
				if( !teamlist[team].numplayers )
					emptyteams++;
				else
					any = qtrue;
			}

			enough = !emptyteams;
		}
		else
		{
			enough = ( teamlist[TEAM_PLAYERS].numplayers > 1 );
			any = ( teamlist[TEAM_PLAYERS].numplayers > 0 );
		}

#ifdef DUEL_ARENA
		// Duel Arena manage this in G_Gametype_DA_CheckRules
		if( game.gametype != GAMETYPE_DA )
		{
#endif
		// countdown running, but not enough players left
		if( match.state == MATCH_STATE_COUNTDOWN && !enough )
		{
			G_PrintMsg( NULL, "Not enough players left. Countdown aborted.\n" );
			G_CenterPrintMsg( NULL, "COUNTDOWN ABORTED\n" );
			G_Match_Autorecord_Cancel();
			match.state = MATCH_STATE_NONE;
			match.endtime = 0;
			match.roundstate = MATCH_STATE_NONE;
			match.roundendtime = 0;
		}
		// match running, but not enough players left
		else if( match.state == MATCH_STATE_PLAYTIME && !enough )
		{
			G_PrintMsg( NULL, "Not enough players left. Match aborted.\n" );
			G_CenterPrintMsg( NULL, "MATCH ABORTED\n" );
			G_EndMatch();
		}
		// re-enable warmup timelimit when we get enough players to start the game
		else if( match.state == MATCH_STATE_WARMUP && g_warmup_timelimit->value && !match.endtime && enough )
		{
			match.starttime = level.time;
			match.endtime = level.time + fabs( g_warmup_timelimit->value * 60 * 1000 );
		}
		// disable warmup timelimit when we no longer have enough players to start the game
		else if( match.state == MATCH_STATE_WARMUP && g_warmup_timelimit->value && match.endtime && !enough )
		{
			match.starttime = level.time;
			match.endtime = 0;
		}
#ifdef DUEL_ARENA
	}
#endif
	}

	//check gametype specific rules
	if( gametypes[game.gametype].checkRules )
		gametypes[game.gametype].checkRules();

	if( G_EachNewSecond() )
		G_CheckNumBots();

	if( G_EachNewMinute() )
		G_CheckEvenTeam();
}
