/*
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

void G_Gametype_GENERIC_SetUpWarmup( void )
{
	// 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_RED; team < TEAM_RED + 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();

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

void G_Gametype_GENERIC_SetUpCountdown( void )
{
	edict_t *ent;

	G_Match_RemoveAllProjectiles();

	if( g_teams_lock->integer ) {
		if( GS_Gametype_IsTeamBased( game.gametype ) ) {
			int	team;
			for( team = TEAM_RED; team < TEAM_RED + 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;

		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;
	}

	G_Match_RespawnAllItems();
	G_Match_RespawnAllClients();
	G_Match_FreeBodyQueue();
	match.pickableItemsMask = GS_Gametype_SpawnableItemMask( game.gametype )|GS_Gametype_DropableItemMask( game.gametype );
	if( g_instagib->integer ) {
		match.pickableItemsMask &= ~G_INSTAGIB_NEGATE_ITEMMASK;
	}

	G_AnnouncerSound( NULL, trap_SoundIndex(va(S_ANNOUNCER_COUNTDOWN_FIGHT_1_to_2, (rand()&1)+1)), GS_MAX_TEAMS, qtrue );
}

void G_Gametype_GENERIC_SetUpEndMatch( void )
{
	//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 + fabs(g_match_extendedtime->value) * 60;
			} 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;
		}
	}

	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
		60, // powerup_respawn
		20, // megahealth_respawn
		// few default settings
		20, // timelimit
		0,  // extended_time
		0,  // scorelimit
		qfalse, // lock_teams
	},

	//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
		60, // powerup_respawn
		20, // megahealth_respawn
		// few default settings
		10, // timelimit
		2,  // extended_time
		0,  // scorelimit
		qfalse, // lock_teams
	},

	//GAMETYPE_TDM
	{
		NULL,												//initialize
		NULL,												//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
		qtrue, // lock_teams
	},

	//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
		qtrue, // lock_teams
	},

	//GAMETYPE_RACE
	{
		G_Gametype_RACE_Init,								//initialize gametype
		G_Gametype_RACE_NewMap,								//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
		60, // powerup_respawn
		20, // megahealth_respawn
		// few default settings
		0,  // timelimit
		0,  // extended_time
		0,  // scorelimit
		qfalse, // lock_teams
	},

	//GAMETYPE_MIDAIR
	{
		NULL,												//initialize
		NULL,												//start a new map
		G_Gametype_MIDAIR_CheckRules,						//think frame
		G_Gametype_MIDAIR_ClientRespawn,					//player respawn
		ClientBeginMultiplayerGame,							//player entered the server
		// if not 1v1 G_Gametype_MIDAIR_ScoreboardMessage,
		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
		60, // powerup_respawn
		20, // megahealth_respawn
		// few default settings
		10, // timelimit
		2,  // extended_time
		0,  // scorelimit
		qfalse, // lock_teams
	},

	{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;
	float			clocktime;

	if( !matchstate )
		return;

	memset( matchstate, 0, sizeof(match_state_t) );
	matchstate->state = match.state;

	// 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
		float timepassed = level.time - match.starttime;
		clocktime = (60 - (int)(timepassed) % 60) - (timepassed - (int)timepassed);
	} else {
		clocktime = level.time - match.starttime;
	}

	if( clocktime <= 0.0f )
		clocktime = 0.0f;

	matchstate->clock_msecs = (unsigned int)( 1000 * clocktime );
	if( matchstate->clock_msecs == 0 ) {
		matchstate->clock_secs = matchstate->clock_mins = 0;
		return;
	}
	matchstate->clock_secs = (unsigned int)( clocktime );
	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_RED; team < TEAM_RED + 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_RED; team < TEAM_RED + g_maxteams->integer; team++ )
		{
			if( !teamlist[team].numplayers )
				continue;

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

//=================
//G_Match_AutorecordCommand
//=================
void G_Match_AutorecordCommand( qboolean activate, qboolean cancel )
{
	static char cmdString[32];
	
	// cancel only make sense for stop
	if(activate==qfalse && cancel==qtrue)
		Q_snprintfz( cmdString, sizeof(cmdString), "autr \"%s", "cancel" );
	else	
		Q_snprintfz( cmdString, sizeof(cmdString), "autr \"%s", activate ? "start" : "stop" );
	trap_ServerCmd( NULL, cmdString );
}

//=================
//G_Match_TimeFinished
//=================
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
//=================
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
		{
			int team;

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

//=================
//G_Match_SuddenDeath
//=================
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

void G_Match_ResetScoreAnnouncement( void )
{
	score_announcement_init = qfalse;
}

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;
}

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;
}

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

	if( match.state == MATCH_STATE_WARMUP && game.gametype != GAMETYPE_RACE ) {
		qboolean readyupwarnings = qfalse;
		// ready up announcements
		int START_TEAM, END_TEAM;
		if( GS_Gametype_IsTeamBased(game.gametype) ) {
			START_TEAM = TEAM_RED;
			END_TEAM = TEAM_RED + 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;

	if( game.gametype == GAMETYPE_RACE )
		return;

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

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

		for( team = TEAM_RED; team < TEAM_RED + 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;
	float oldstarttime = match.starttime;

	match.state++;
	match.starttime = level.time;

	if( match.state == MATCH_STATE_WARMUP )
	{
		qboolean enough;

		advance_queue = qfalse;

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

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

		// even if warmup is not enabled, we use it until enough players show up
		if( g_warmup_enabled->integer || !enough )
		{
			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 + fabs(g_warmup_timelimit->value * 60);
			else
				match.endtime = 0.0f;

			//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_AutorecordCommand(qtrue, qfalse);

		advance_queue = qtrue;

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

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

	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 + fabs(60 * g_timelimit->value);
		else
			match.endtime = 0.0f;

		//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(4); // postmatch time in seconds

		G_Timeout_Reset();

		//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;
		}
		match.forceExit = qfalse;
	}

	if( match.state == MATCH_STATE_WAITEXIT )
	{
		G_Match_AutorecordCommand(qfalse,qfalse);
		if( advance_queue ) {
			G_Teams_AdvanceChallengersQueue();
			advance_queue = qtrue;
		}

		match.endtime = level.time + fabs(25); //maximum time to wait before changing map in seconds
		level.exitnow = 0;
	}

	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.0f;
	}

	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_RED + 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" );
		match.state = MATCH_STATE_NONE;
		match.endtime = 0.0f;
	}
}

//=================
//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();
}


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

//=================
//G_Match_RespawnAllItems
//Note: respawns the items right this frame
//=================
void DoRespawn (edict_t *ent);
void MegaHealth_think (edict_t *self);
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.timemsec )
		{
			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);
	}
}

//=================
//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.solid != SOLID_NOT )
		{
			if( ent->s.type == ET_ROCKET || ent->s.type == ET_GRENADE || ent->s.type == ET_ELECTRO_WEAK ||
					ent->s.type == ET_BLASTER || !Q_stricmp(ent->classname, "plasma"))
				G_FreeEdict(ent);
		}
	}
}

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

	G_Gametype_Update();

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

		G_FreeEdict(ent);
		ent->freetime = 0; // allow overwriting these right away
	}

	G_SpawnMapEntities( qfalse );
}

//=================
//G_Match_FreeBodyQueue
//=================
void G_Match_FreeBodyQueue( void )
{
	edict_t *ent;
	
	for( ent = game.edicts + game.maxclients + 1; ENTNUM(ent) < game.maxclients + BODY_QUEUE_SIZE + 1; ent++ ) 
	{
		if( !ent->r.inuse || ent->r.svflags & SVF_NOCLIENT )
			continue;
		
		if( !Q_stricmp( ent->classname, "body" ) )
		{
			trap_UnlinkEntity(ent);
			
			ent->deadflag = DEAD_NO;
			ent->movetype = MOVETYPE_NOCLIP;
			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;
			
			trap_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;
	
	for( ent = game.edicts + 1; PLAYERNUM(ent) < game.maxclients; ent++ )
	{
		if( !ent->r.inuse )
			continue;

		if( ent->s.team == TEAM_SPECTATOR ) //ignore spectators
			continue;

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

		//powerup info
		ent->r.client->quad_timeout = 0;
		ent->r.client->grenade_blew_up = qfalse;
		ent->r.client->grenade_time = 0;

		//spawn
		ent->movetype = MOVETYPE_NOCLIP;
		respawn(ent);
	}
}


//=================
//G_Match_NewMap
//=================
void G_Match_NewMap( void )
{
	match.state = MATCH_STATE_NONE;
	match.endtime = 0.0f;

	//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
	{
		char configstring[MAX_CONFIGSTRING_CHARS];
		
		Q_snprintfz( configstring, sizeof(configstring), "%i %i",
			G_Gametype_hasChallengersQueue(game.gametype),
			g_maxteams->integer
			);
		trap_ConfigString( CS_SERVERSETTINGS, configstring );
	}
}


//=================
//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;

	client = ent->r.client;
	
	if( !spawnpoint )
		spawnpoint = world;
	
	VectorCopy( spawnpoint->s.origin, ent->s.origin );
	client->ps.pmove.origin[0] = spawnpoint->s.origin[0]*16;
	client->ps.pmove.origin[1] = spawnpoint->s.origin[1]*16;
	client->ps.pmove.origin[2] = spawnpoint->s.origin[2]*16;
	VectorCopy( spawnpoint->s.angles, client->ps.viewangles );
	client->ps.pmove.pm_type = PM_FREEZE;
	client->ps.pmove.pm_flags |= PMF_NO_PREDICTION;
	client->chase.active = qfalse;
	G_Gametype_CTF_ResetClientFlag( ent );
	G_ClearPlayerStateEvents( client );

	client->ps.POVnum = 0;

	// clean up powerup info
	client->quad_timeout = 0;
	client->grenade_blew_up = qfalse;
	client->grenade_time = 0;

	ent->viewheight = 0;
	ent->s.modelindex = 0;
	ent->s.modelindex2 = 0;
	ent->s.effects = 0;
	ent->s.sound = 0;
	ent->s.light = 0;
	ent->r.solid = SOLID_NOT;
}

//=================
//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) )
			respawn(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 = match.endtime - level.time;
	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
		}
	} 
	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.client )
			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", 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 (DeathMatch)\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();
}

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 || !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 -= (1.0f * game.framemsec) / 1000.0f;
			if( e->health < e->max_health )
				e->health = e->max_health;
		}
	}
}

#ifdef ARMOR_SYSTEM_OF_THE_DAY
//=================
//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 -= (1.0f * game.framemsec) / 1000.0f;
			if( e->r.client->armor < 150 )
				e->r.client->armor = 150;
		}
	}
}
#endif

//=================
//G_Gametype_ShortName
//=================
char *G_Gametype_ScoreboardMessage( edict_t *ent, edict_t *killer )
{
	if( gametypes[game.gametype].scoreboardMessage )
		return gametypes[game.gametype].scoreboardMessage( ent, killer );

	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).
//=================
void G_Gametype_CTF_FragBonuses(edict_t *targ, edict_t *inflictor, edict_t *attacker, int mod);
void G_Gametype_TDM_FragBonuses(edict_t *targ, edict_t *inflictor, edict_t *attacker, int mod);

void G_DropClientBackPack( edict_t *self );
void TossClientWeapon (edict_t *self);
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 = trap_PointContents( targ->s.origin );

	//clients / bots: add the frag to scores
	if( attacker && match.state == MATCH_STATE_PLAYTIME && !(targ->r.svflags & SVF_CORPSE) )
	{
		G_ScoreboardEvent();

		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_MIDAIR ) 
		{
			// 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--;
				teamlist[targ->s.team].teamplayerscores--;
				teamlist[targ->s.team].teamscore--;
				
			} else if( attacker == targ ) { // self fragged (not going to happen in midair, but well...
			
				match.scores[PLAYERNUM(attacker)].score--;
				teamlist[attacker->s.team].teamplayerscores--;
				teamlist[attacker->s.team].teamscore--;
				match.scores[PLAYERNUM(attacker)].deaths++;
				match.scores[PLAYERNUM(attacker)].suicides++;

			} else {

				//trace_t trace;
				int		score;

				// normal rox goes 1000 units/sec
				// 0.2, 0.4, 0.6 0,8 1 for score from 1 to 5
				score=1000*(level.time-inflictor->timestamp)/200;
				
				if(score==0)
					score=1;
				else if(score>5)
					score=5;

				//G_Printf(">> %d\n", score);
				switch(score)
				{
				case 2:
					{
						G_Printf( "%s%s got%s midair\n", attacker->r.client->pers.netname, 
							S_COLOR_WHITE, "" );
						G_AnnouncerSound( NULL, trap_SoundIndex(va(S_ANNOUNCER_MIDAIR_MIDAIR_1_to_2, (rand()&1)+1)), 
							GS_MAX_TEAMS, qtrue );
					}
					break;
				/*
				case 3:
					{
						G_Printf( "%s%s got%s midair\n", attacker->r.client->pers.netname, 
							S_COLOR_WHITE, " silver" );
					}
					break;
				*/
				case 3:
					{
						G_Printf( "%s%s got%s midair\n", attacker->r.client->pers.netname, 
							S_COLOR_WHITE, " golden" );
						G_AnnouncerSound( NULL, trap_SoundIndex(va(S_ANNOUNCER_MIDAIR_GOLD_1_to_2, (rand()&1)+1)), 
							GS_MAX_TEAMS, qtrue );
					}
					break;
				case 4:
					{
						G_Printf( "%s%s got%s midair\n", attacker->r.client->pers.netname, 
							S_COLOR_WHITE, " diamond" );
						G_AnnouncerSound( NULL, trap_SoundIndex(va(S_ANNOUNCER_MIDAIR_DIAMOND_1_to_2, (rand()&1)+1)), 
							GS_MAX_TEAMS, qtrue );
					}
					break;

					// King of bongo
				case 5:
					{
						G_Printf( "%s%s is the king of Bongos\n", attacker->r.client->pers.netname, 
							S_COLOR_WHITE);
						G_AnnouncerSound( NULL, trap_SoundIndex(va(S_ANNOUNCER_MIDAIR_BONGO_1_to_2, (rand()&1)+1)), 
							GS_MAX_TEAMS, qtrue );
					}
					break;
				}

				match.scores[PLAYERNUM(attacker)].score += score;
				teamlist[attacker->s.team].teamplayerscores += score;
				teamlist[attacker->s.team].teamscore += score;
				match.scores[PLAYERNUM(attacker)].kills++;
				if( targ->r.client )
					match.scores[PLAYERNUM(targ)].deaths++;
			}
			if( targ->r.client && !(contents & CONTENTS_NODROP) ) 
			{
				edict_t		*drop;
				float		yawoffset;

				//create the entity
				yawoffset = random() * targ->r.client->v_angle[YAW] * 0.5;
				targ->r.client->v_angle[YAW] -= yawoffset;
				drop = Drop_Item( targ, game.items[HEALTH_MEDIUM] );
				targ->r.client->v_angle[YAW] += yawoffset;
				if( drop ) {
					drop->spawnflags |= DROPPED_PLAYER_ITEM;
				}
			}
		}
		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_in_race==qtrue)
		{
			// reset flag
			targ->r.client->resp.race_in_race=qfalse;
	
			// dispatch stop event to client to cancel the demos
			G_Match_AutorecordCommand(qfalse,qtrue);
		}
	}

	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;
	if( !item )
		return qfalse;

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

	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 ) {
	int itemmask;
	if( !item )
		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 )
{
	if( game.gametype == GAMETYPE_MIDAIR 
		|| game.gametype == GAMETYPE_RACE )
		return qfalse;

	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;

	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 )
		return gametypes[game.gametype].ammo_respawn;
	
	if( item->type & IT_WEAPON )
		return gametypes[game.gametype].weapon_respawn;
	
	if( item->tag == HEALTH_MEGA )
		return gametypes[game.gametype].megahealth_respawn;
	
	if( item->type & IT_HEALTH )
		return gametypes[game.gametype].health_respawn;
	
	if( item->type & IT_ARMOR )
		return gametypes[game.gametype].armor_respawn;
	
	if( item->type & IT_POWERUP )
		return gametypes[game.gametype].powerup_respawn;

	return item->quantity;
}

//=================
//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
//=================
int G_Gametype_ClientRespawn( edict_t *self )
{
	if( gametypes[game.gametype].respawn )
		return gametypes[game.gametype].respawn(self);

	return qfalse;
}

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

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

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

	if( game.numBots == g_numbots->integer )
		return;

	// check sanity of g_numbots
	if( g_numbots->integer < 0 || g_numbots->integer >= game.maxclients ) {
		trap_Cvar_Set( "g_numbots", va("%i", game.numBots) );
		G_PrintMsg( NULL, "There's no room for more bots\n" );

	} else if( g_numbots->integer > game.numBots ) {
		for( i = game.numBots; i < g_numbots->integer; i++ )
			BOT_SpawnBot(NULL);

	} else 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, "G_CheckNumBots");
				break;
			}
		}
	}
}

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

	//fixme: should scorelimit be inside each gametype rules?
	if( G_Match_ScorelimitHit() ) {
		G_Match_SetUpNextState();
		return;
	}

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

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

	G_Match_ScoreAnnouncement();

	if( game.gametype != GAMETYPE_RACE )
	{
		qboolean any = qfalse;
		qboolean enough;

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

			for( team = TEAM_RED; team < TEAM_RED + 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);
		}

		// match running, but not enough players left
		// we allow match continue if it's in timeout and we got atleast one person left
		if( match.state == MATCH_STATE_PLAYTIME && (!any || (!enough && !gtimeout.active)) )
		{
			G_PrintMsg( NULL, "Not enough players left. Match aborted.\n" );
			G_CenterPrintMsg( NULL, "MATCH ABORTED\n" );
			G_EndMatch();
		}
		// if we are in warmup even though warmup is not enabled, it's because there wasn't enough players
		// so if we get enough players, continue to gametime
		else if( match.state == MATCH_STATE_WARMUP && !g_warmup_enabled->integer && enough )
		{
			G_Match_SetUpNextState();
		}
		// 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);
		}
		// 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.0f;
		}
	}

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

	if( G_EachNewSecond() )
		G_CheckNumBots();
}
