/*
 * Copyright (C) 1997-2001 Id Software, Inc.
 * Copyright (c) ZeniMax Media 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.
 *
 * =======================================================================
 *
 * CTF specific code.
 *
 * =======================================================================
 */

#include "header/local.h"
#include "monster/misc/player.h"

typedef enum match_s
{
	MATCH_NONE,
	MATCH_SETUP,
	MATCH_PREGAME,
	MATCH_GAME,
	MATCH_POST
} match_t;

typedef enum
{
	ELECT_NONE,
	ELECT_MATCH,
	ELECT_ADMIN,
	ELECT_MAP
} elect_t;

typedef struct ctfgame_s
{
	int team1, team2;
	int total1, total2; /* these are only set when going into intermission! */
	float last_flag_capture;
	int last_capture_team;

	match_t match;      /* match state */
	float matchtime;    /* time for match start/end (depends on state) */
	int lasttime;       /* last time update */
	qboolean countdown; /* has audio countdown started? */

	elect_t election;   /* election type */
	edict_t *etarget;   /* for admin election, who's being elected */
	char elevel[32];    /* for map election, target level */
	int evotes;         /* votes so far */
	int needvotes;      /* votes needed */
	float electtime;    /* remaining time until election times out */
	char emsg[256];     /* election name */
	int warnactive; /* true if stat string 30 is active */

	ghost_t ghosts[MAX_CLIENTS]; /* ghost codes */
} ctfgame_t;

ctfgame_t ctfgame;

cvar_t *ctf;
cvar_t *ctf_forcejoin;

cvar_t *competition;
cvar_t *matchlock;
cvar_t *electpercentage;
cvar_t *matchtime;
cvar_t *matchsetuptime;
cvar_t *matchstarttime;
cvar_t *admin_password;
cvar_t *allow_admin;
cvar_t *warp_list;
cvar_t *warn_unbalanced;

/* Index for various CTF pics, this saves us
 * from calling gi.imageindex all the time
 * and saves a few CPU cycles since we don't
 * have to do a bunch of string compares all
 * the time. * These are set in CTFPrecache()
 * called from worldspawn */
int imageindex_i_ctf1;
int imageindex_i_ctf2;
int imageindex_i_ctf1d;
int imageindex_i_ctf2d;
int imageindex_i_ctf1t;
int imageindex_i_ctf2t;
int imageindex_i_ctfj;
int imageindex_sbfctf1;
int imageindex_sbfctf2;
int imageindex_ctfsb1;
int imageindex_ctfsb2;

char *ctf_statusbar =
	"yb	-24 "

	/* health */
	"xv	0 "
	"hnum "
	"xv	50 "
	"pic 0 "

	/* ammo */
	"if 2 "
	"	xv	100 "
	"	anum "
	"	xv	150 "
	"	pic 2 "
	"endif "

	/* armor */
	"if 4 "
	"	xv	200 "
	"	rnum "
	"	xv	250 "
	"	pic 4 "
	"endif "

	/* selected item */
	"if 6 "
	"	xv	296 "
	"	pic 6 "
	"endif "

	"yb	-50 "

	/* picked up item */
	"if 7 "
	"	xv	0 "
	"	pic 7 "
	"	xv	26 "
	"	yb	-42 "
	"	stat_string 8 "
	"	yb	-50 "
	"endif "

	/* timer */
	"if 9 "
	"xv 246 "
	"num 2 10 "
	"xv 296 "
	"pic 9 "
	"endif "

	/*  help / weapon icon */
	"if 11 "
	"xv 148 "
	"pic 11 "
	"endif "

	/*  frags */
	"xr	-50 "
	"yt 2 "
	"num 3 14 "

	/* tech */
	"yb -129 "
	"if 26 "
	"xr -26 "
	"pic 26 "
	"endif "

	/* red team */
	"yb -102 "
	"if 17 "
	"xr -26 "
	"pic 17 "
	"endif "
	"xr -62 "
	"num 2 18 "
	/* joined overlay */
	"if 22 "
	"yb -104 "
	"xr -28 "
	"pic 22 "
	"endif "

	/* blue team */
	"yb -75 "
	"if 19 "
	"xr -26 "
	"pic 19 "
	"endif "
	"xr -62 "
	"num 2 20 "
	"if 23 "
	"yb -77 "
	"xr -28 "
	"pic 23 "
	"endif "

	/* have flag graph */
	"if 21 "
	"yt 26 "
	"xr -24 "
	"pic 21 "
	"endif "

	/* id view state */
	"if 27 "
	"xv 112 "
	"yb -58 "
	"stat_string 27 "
	"endif "

	"if 29 "
	"xv 96 "
	"yb -58 "
	"pic 29 "
	"endif "

	"if 28 "
	"xl 0 "
	"yb -78 "
	"stat_string 28 "
	"endif "

	"if 30 "
	"xl 0 "
	"yb -88 "
	"stat_string 30 "
	"endif "
	;

static char *tnames[] = {
	"item_tech1", "item_tech2", "item_tech3", "item_tech4",
	NULL
};

/*--------------------------------------------------------------------------*/

/*
 * Returns entities that have
 * origins within a spherical area
 */
static edict_t *
loc_findradius(edict_t *from, vec3_t org, float rad)
{
	vec3_t eorg;
	int j;

	if (!from)
	{
		from = g_edicts;
	}
	else
	{
		from++;
	}

	for ( ; from < &g_edicts[globals.num_edicts]; from++)
	{
		if (!from->inuse)
		{
			continue;
		}

		for (j = 0; j < 3; j++)
		{
			eorg[j] = org[j] - (from->s.origin[j] +
					   (from->mins[j] + from->maxs[j]) * 0.5);
		}

		if (VectorLength(eorg) > rad)
		{
			continue;
		}

		return from;
	}

	return NULL;
}

static void
loc_buildboxpoints(vec3_t p[8], vec3_t org, vec3_t mins, vec3_t maxs)
{
	VectorAdd(org, mins, p[0]);
	VectorCopy(p[0], p[1]);
	p[1][0] -= mins[0];
	VectorCopy(p[0], p[2]);
	p[2][1] -= mins[1];
	VectorCopy(p[0], p[3]);
	p[3][0] -= mins[0];
	p[3][1] -= mins[1];
	VectorAdd(org, maxs, p[4]);
	VectorCopy(p[4], p[5]);
	p[5][0] -= maxs[0];
	VectorCopy(p[0], p[6]);
	p[6][1] -= maxs[1];
	VectorCopy(p[0], p[7]);
	p[7][0] -= maxs[0];
	p[7][1] -= maxs[1];
}

static qboolean
loc_CanSee(edict_t *targ, edict_t *inflictor)
{
	trace_t trace;
	vec3_t targpoints[8];
	int i;
	vec3_t viewpoint;

	/* bmodels need special checking because their origin is 0,0,0 */
	if (targ->movetype == MOVETYPE_PUSH)
	{
		return false; /* bmodels not supported */
	}

	loc_buildboxpoints(targpoints, targ->s.origin, targ->mins, targ->maxs);

	VectorCopy(inflictor->s.origin, viewpoint);
	viewpoint[2] += inflictor->viewheight;

	for (i = 0; i < 8; i++)
	{
		trace = gi.trace(viewpoint, vec3_origin, vec3_origin,
				targpoints[i], inflictor, MASK_SOLID);

		if (trace.fraction == 1.0)
		{
			return true;
		}
	}

	return false;
}

/*--------------------------------------------------------------------------*/

static gitem_t *flag1_item;
static gitem_t *flag2_item;

void
CTFSpawn(void)
{
	if (!flag1_item)
	{
		flag1_item = FindItemByClassname("item_flag_team1");
	}

	if (!flag2_item)
	{
		flag2_item = FindItemByClassname("item_flag_team2");
	}

	memset(&ctfgame, 0, sizeof(ctfgame));
	CTFSetupTechSpawn();

	if (competition->value > 1)
	{
		ctfgame.match = MATCH_SETUP;
		ctfgame.matchtime = level.time + matchsetuptime->value * 60;
	}
}

void
CTFInit(void)
{
	ctf = gi.cvar("ctf", "0", CVAR_SERVERINFO);
	ctf_forcejoin = gi.cvar("ctf_forcejoin", "", 0);
	competition = gi.cvar("competition", "0", CVAR_SERVERINFO);
	matchlock = gi.cvar("matchlock", "1", CVAR_SERVERINFO);
	electpercentage = gi.cvar("electpercentage", "66", 0);
	matchtime = gi.cvar("matchtime", "20", CVAR_SERVERINFO);
	matchsetuptime = gi.cvar("matchsetuptime", "10", 0);
	matchstarttime = gi.cvar("matchstarttime", "20", 0);
	admin_password = gi.cvar("admin_password", "", 0);
	allow_admin = gi.cvar("allow_admin", "1", 0);
	warp_list = gi.cvar("warp_list", "q2ctf1 q2ctf2 q2ctf3 q2ctf4 q2ctf5", 0);
	warn_unbalanced = gi.cvar("warn_unbalanced", "1", 0);
}

void
CTFPrecache(void)
{
	imageindex_i_ctf1 = gi.imageindex("i_ctf1");
	imageindex_i_ctf2 = gi.imageindex("i_ctf2");
	imageindex_i_ctf1d = gi.imageindex("i_ctf1d");
	imageindex_i_ctf2d = gi.imageindex("i_ctf2d");
	imageindex_i_ctf1t = gi.imageindex("i_ctf1t");
	imageindex_i_ctf2t = gi.imageindex("i_ctf2t");
	imageindex_i_ctfj = gi.imageindex("i_ctfj");
	imageindex_sbfctf1 = gi.imageindex("sbfctf1");
	imageindex_sbfctf2 = gi.imageindex("sbfctf2");
	imageindex_ctfsb1 = gi.imageindex("ctfsb1");
	imageindex_ctfsb2 = gi.imageindex("ctfsb2");
}

/*--------------------------------------------------------------------------*/

char *
CTFTeamName(int team)
{
	switch (team)
	{
		case CTF_TEAM1:
			return "RED";
		case CTF_TEAM2:
			return "BLUE";
	}

	return "UNKNOWN";
}

char *
CTFOtherTeamName(int team)
{
	switch (team)
	{
		case CTF_TEAM1:
			return "BLUE";
		case CTF_TEAM2:
			return "RED";
	}

	return "UNKNOWN";
}

static int
CTFOtherTeam(int team)
{
	switch (team)
	{
		case CTF_TEAM1:
			return CTF_TEAM2;
		case CTF_TEAM2:
			return CTF_TEAM1;
	}

	return -1; /* invalid value */
}

/*--------------------------------------------------------------------------*/

edict_t *SelectRandomDeathmatchSpawnPoint(void);
edict_t *SelectFarthestDeathmatchSpawnPoint(void);
float PlayersRangeFromSpot(edict_t *spot);

void
CTFAssignSkin(edict_t *ent, char *s)
{
	int playernum = ent - g_edicts - 1;
	char *p;
	char t[64];

	Com_sprintf(t, sizeof(t), "%s", s);

	if ((p = strchr(t, '/')) != NULL)
	{
		p[1] = 0;
	}
	else
	{
		strcpy(t, "male/");
	}

	switch (ent->client->resp.ctf_team)
	{
		case CTF_TEAM1:
			gi.configstring(CS_PLAYERSKINS + playernum, va("%s\\%s%s",
						ent->client->pers.netname, t, CTF_TEAM1_SKIN));
			break;
		case CTF_TEAM2:
			gi.configstring(CS_PLAYERSKINS + playernum,
				va("%s\\%s%s", ent->client->pers.netname, t, CTF_TEAM2_SKIN));
			break;
		default:
			gi.configstring(CS_PLAYERSKINS + playernum,
				va("%s\\%s", ent->client->pers.netname, s));
			break;
	}
}

void
CTFAssignTeam(gclient_t *who)
{
	edict_t *player;
	int i;
	int team1count = 0, team2count = 0;

	who->resp.ctf_state = 0;

	if (!((int)dmflags->value & DF_CTF_FORCEJOIN))
	{
		who->resp.ctf_team = CTF_NOTEAM;
		return;
	}

	for (i = 1; i <= maxclients->value; i++)
	{
		player = &g_edicts[i];

		if (!player->inuse || (player->client == who))
		{
			continue;
		}

		switch (player->client->resp.ctf_team)
		{
			case CTF_TEAM1:
				team1count++;
				break;
			case CTF_TEAM2:
				team2count++;
		}
	}

	if (team1count < team2count)
	{
		who->resp.ctf_team = CTF_TEAM1;
	}
	else if (team2count < team1count)
	{
		who->resp.ctf_team = CTF_TEAM2;
	}
	else if (rand() & 1)
	{
		who->resp.ctf_team = CTF_TEAM1;
	}
	else
	{
		who->resp.ctf_team = CTF_TEAM2;
	}
}

/*
 * Go to a ctf point, but NOT the
 * two points closest to other players
 */
edict_t *
SelectCTFSpawnPoint(edict_t *ent)
{
	edict_t *spot, *spot1, *spot2;
	int count = 0;
	int selection;
	float range, range1, range2;
	char *cname;

	if (ent->client->resp.ctf_state)
	{
		if ((int)(dmflags->value) & DF_SPAWN_FARTHEST)
		{
			return SelectFarthestDeathmatchSpawnPoint();
		}
		else
		{
			return SelectRandomDeathmatchSpawnPoint();
		}
	}

	ent->client->resp.ctf_state++;

	switch (ent->client->resp.ctf_team)
	{
		case CTF_TEAM1:
			cname = "info_player_team1";
			break;
		case CTF_TEAM2:
			cname = "info_player_team2";
			break;
		default:
			return SelectRandomDeathmatchSpawnPoint();
	}

	spot = NULL;
	range1 = range2 = 99999;
	spot1 = spot2 = NULL;

	while ((spot = G_Find(spot, FOFS(classname), cname)) != NULL)
	{
		count++;
		range = PlayersRangeFromSpot(spot);

		if (range < range1)
		{
			range1 = range;
			spot1 = spot;
		}
		else if (range < range2)
		{
			range2 = range;
			spot2 = spot;
		}
	}

	if (!count)
	{
		return SelectRandomDeathmatchSpawnPoint();
	}

	if (count <= 2)
	{
		spot1 = spot2 = NULL;
	}
	else
	{
		count -= 2;
	}

	selection = rand() % count;

	spot = NULL;

	do
	{
		spot = G_Find(spot, FOFS(classname), cname);

		if ((spot == spot1) || (spot == spot2))
		{
			selection++;
		}
	}
	while (selection--);

	return spot;
}

/*------------------------------------------------------------------------*/

/*
 * Calculate the bonuses for flag defense,
 * flag carrier defense, etc. Note that b
 * onuses are not cumaltive. You get one,
 * they are in importance order.
 */
void
CTFFragBonuses(edict_t *targ, edict_t *inflictor, edict_t *attacker)
{
	int i;
	edict_t *ent;
	gitem_t *flag_item, *enemy_flag_item;
	int otherteam;
	edict_t *flag, *carrier;
	char *c;
	vec3_t v1, v2;

	if (targ->client && attacker->client)
	{
		if (attacker->client->resp.ghost)
		{
			if (attacker != targ)
			{
				attacker->client->resp.ghost->kills++;
			}
		}

		if (targ->client->resp.ghost)
		{
			targ->client->resp.ghost->deaths++;
		}
	}

	/* no bonus for fragging yourself */
	if (!targ->client || !attacker->client || (targ == attacker))
	{
		return;
	}

	otherteam = CTFOtherTeam(targ->client->resp.ctf_team);

	if (otherteam < 0)
	{
		return; /* whoever died isn't on a team */
	}

	/* same team, if the flag at base, check to he has the enemy flag */
	if (targ->client->resp.ctf_team == CTF_TEAM1)
	{
		flag_item = flag1_item;
		enemy_flag_item = flag2_item;
	}
	else
	{
		flag_item = flag2_item;
		enemy_flag_item = flag1_item;
	}

	/* did the attacker frag the flag carrier? */
	if (targ->client->pers.inventory[ITEM_INDEX(enemy_flag_item)])
	{
		attacker->client->resp.ctf_lastfraggedcarrier = level.time;
		attacker->client->resp.score += CTF_FRAG_CARRIER_BONUS;
		gi.cprintf(attacker, PRINT_MEDIUM,
				"BONUS: %d points for fragging enemy flag carrier.\n",
				CTF_FRAG_CARRIER_BONUS);

		/* the target had the flag, clear
		   the hurt carrier field on the
		   other team */
		for (i = 1; i <= maxclients->value; i++)
		{
			ent = g_edicts + i;

			if (ent->inuse && (ent->client->resp.ctf_team == otherteam))
			{
				ent->client->resp.ctf_lasthurtcarrier = 0;
			}
		}

		return;
	}

	if (targ->client->resp.ctf_lasthurtcarrier &&
		(level.time - targ->client->resp.ctf_lasthurtcarrier <
		 CTF_CARRIER_DANGER_PROTECT_TIMEOUT) &&
		!attacker->client->pers.inventory[ITEM_INDEX(flag_item)])
	{
		/* attacker is on the same team
		   as the flag carrier and
		   fragged a guy who hurt our
		   flag carrier */
		attacker->client->resp.score += CTF_CARRIER_DANGER_PROTECT_BONUS;
		gi.bprintf(PRINT_MEDIUM,
				"%s defends %s's flag carrier against an aggressive enemy\n",
				attacker->client->pers.netname,
				CTFTeamName(attacker->client->resp.ctf_team));

		if (attacker->client->resp.ghost)
		{
			attacker->client->resp.ghost->carrierdef++;
		}

		return;
	}

	/* flag and flag carrier area defense bonuses
	   we have to find the flag and carrier entities
	   find the flag */
	switch (attacker->client->resp.ctf_team)
	{
		case CTF_TEAM1:
			c = "item_flag_team1";
			break;
		case CTF_TEAM2:
			c = "item_flag_team2";
			break;
		default:
			return;
	}

	flag = NULL;

	while ((flag = G_Find(flag, FOFS(classname), c)) != NULL)
	{
		if (!(flag->spawnflags & DROPPED_ITEM))
		{
			break;
		}
	}

	if (!flag)
	{
		return; /* can't find attacker's flag */
	}

	carrier = NULL;

	/* find attacker's team's flag carrier */
	for (i = 1; i <= maxclients->value; i++)
	{
		carrier = g_edicts + i;

		if (carrier->inuse &&
			carrier->client->pers.inventory[ITEM_INDEX(flag_item)])
		{
			break;
		}

		carrier = NULL;
	}

	/* check to see if we are defending the base's flag */
	VectorSubtract(targ->s.origin, flag->s.origin, v1);
	VectorSubtract(attacker->s.origin, flag->s.origin, v2);

	if (((VectorLength(v1) < CTF_TARGET_PROTECT_RADIUS) ||
		 (VectorLength(v2) < CTF_TARGET_PROTECT_RADIUS) ||
		 loc_CanSee(flag, targ) || loc_CanSee(flag, attacker)) &&
		(attacker->client->resp.ctf_team != targ->client->resp.ctf_team))
	{
		/* we defended the base flag */
		attacker->client->resp.score += CTF_FLAG_DEFENSE_BONUS;

		if (flag->solid == SOLID_NOT)
		{
			gi.bprintf(PRINT_MEDIUM, "%s defends the %s base.\n",
					attacker->client->pers.netname,
					CTFTeamName(attacker->client->resp.ctf_team));
		}
		else
		{
			gi.bprintf(PRINT_MEDIUM, "%s defends the %s flag.\n",
					attacker->client->pers.netname,
					CTFTeamName(attacker->client->resp.ctf_team));
		}

		if (attacker->client->resp.ghost)
		{
			attacker->client->resp.ghost->basedef++;
		}

		return;
	}

	if (carrier && (carrier != attacker))
	{
		VectorSubtract(targ->s.origin, carrier->s.origin, v1);
		VectorSubtract(attacker->s.origin, carrier->s.origin, v1);

		if ((VectorLength(v1) < CTF_ATTACKER_PROTECT_RADIUS) ||
			(VectorLength(v2) < CTF_ATTACKER_PROTECT_RADIUS) ||
			loc_CanSee(carrier, targ) || loc_CanSee(carrier, attacker))
		{
			attacker->client->resp.score += CTF_CARRIER_PROTECT_BONUS;
			gi.bprintf(PRINT_MEDIUM, "%s defends the %s's flag carrier.\n",
					attacker->client->pers.netname,
					CTFTeamName(attacker->client->resp.ctf_team));

			if (attacker->client->resp.ghost)
			{
				attacker->client->resp.ghost->carrierdef++;
			}

			return;
		}
	}
}

void
CTFCheckHurtCarrier(edict_t *targ, edict_t *attacker)
{
	gitem_t *flag_item;

	if (!targ->client || !attacker->client)
	{
		return;
	}

	if (!flag1_item || !flag2_item)
	{
		return;
	}

	if (targ->client->resp.ctf_team == CTF_TEAM1)
	{
		flag_item = flag2_item;
	}
	else
	{
		flag_item = flag1_item;
	}

	if (targ->client->pers.inventory[ITEM_INDEX(flag_item)] &&
		(targ->client->resp.ctf_team != attacker->client->resp.ctf_team))
	{
		attacker->client->resp.ctf_lasthurtcarrier = level.time;
	}
}

/*------------------------------------------------------------------------*/

void
CTFResetFlag(int ctf_team)
{
	char *c;
	edict_t *ent;

	switch (ctf_team)
	{
		case CTF_TEAM1:
			c = "item_flag_team1";
			break;
		case CTF_TEAM2:
			c = "item_flag_team2";
			break;
		default:
			return;
	}

	ent = NULL;

	while ((ent = G_Find(ent, FOFS(classname), c)) != NULL)
	{
		if (ent->spawnflags & DROPPED_ITEM)
		{
			G_FreeEdict(ent);
		}
		else
		{
			ent->svflags &= ~SVF_NOCLIENT;
			ent->solid = SOLID_TRIGGER;
			gi.linkentity(ent);
			ent->s.event = EV_ITEM_RESPAWN;
		}
	}
}

static void
CTFResetFlags(void)
{
	CTFResetFlag(CTF_TEAM1);
	CTFResetFlag(CTF_TEAM2);
}

qboolean
CTFPickup_Flag(edict_t *ent, edict_t *other)
{
	int ctf_team;
	int i;
	edict_t *player;
	gitem_t *flag_item, *enemy_flag_item;

	/* figure out what team this flag is */
	if (strcmp(ent->classname, "item_flag_team1") == 0)
	{
		ctf_team = CTF_TEAM1;
	}
	else if (strcmp(ent->classname, "item_flag_team2") == 0)
	{
		ctf_team = CTF_TEAM2;
	}
	else
	{
		gi.cprintf(ent, PRINT_HIGH, "Don't know what team the flag is on.\n");
		return false;
	}

	/* same team, if the flag at base,
	   check to he has the enemy flag */
	if (ctf_team == CTF_TEAM1)
	{
		flag_item = flag1_item;
		enemy_flag_item = flag2_item;
	}
	else
	{
		flag_item = flag2_item;
		enemy_flag_item = flag1_item;
	}

	if (ctf_team == other->client->resp.ctf_team)
	{
		if (!(ent->spawnflags & DROPPED_ITEM))
		{
			/* the flag is at home base. If the player
			   has the enemy flag, he's just won! */
			if (other->client->pers.inventory[ITEM_INDEX(enemy_flag_item)])
			{
				gi.bprintf(PRINT_HIGH, "%s captured the %s flag!\n",
						other->client->pers.netname, CTFOtherTeamName(ctf_team));
				other->client->pers.inventory[ITEM_INDEX(enemy_flag_item)] = 0;

				ctfgame.last_flag_capture = level.time;
				ctfgame.last_capture_team = ctf_team;

				if (ctf_team == CTF_TEAM1)
				{
					ctfgame.team1++;
				}
				else
				{
					ctfgame.team2++;
				}

				gi.sound(ent, CHAN_RELIABLE + CHAN_NO_PHS_ADD + CHAN_VOICE,
						gi.soundindex("ctf/flagcap.wav"), 1, ATTN_NONE, 0);

				/* other gets another 10 frag bonus */
				other->client->resp.score += CTF_CAPTURE_BONUS;

				if (other->client->resp.ghost)
				{
					other->client->resp.ghost->caps++;
				}

				/* Ok, let's do the player loop, hand out the bonuses */
				for (i = 1; i <= maxclients->value; i++)
				{
					player = &g_edicts[i];

					if (!player->inuse)
					{
						continue;
					}

					if (player->client->resp.ctf_team !=
						other->client->resp.ctf_team)
					{
						player->client->resp.ctf_lasthurtcarrier = -5;
					}
					else if (player->client->resp.ctf_team ==
							 other->client->resp.ctf_team)
					{
						if (player != other)
						{
							player->client->resp.score += CTF_TEAM_BONUS;
						}

						/* award extra points for capture assists */
						if (player->client->resp.ctf_lastreturnedflag +
							CTF_RETURN_FLAG_ASSIST_TIMEOUT > level.time)
						{
							gi.bprintf( PRINT_HIGH, "%s gets an assist for returning the flag!\n",
									player->client->pers.netname);
							player->client->resp.score += CTF_RETURN_FLAG_ASSIST_BONUS;
						}

						if (player->client->resp.ctf_lastfraggedcarrier +
							CTF_FRAG_CARRIER_ASSIST_TIMEOUT > level.time)
						{
							gi.bprintf( PRINT_HIGH, "%s gets an assist for fragging the flag carrier!\n",
									player->client->pers.netname);
							player->client->resp.score += CTF_FRAG_CARRIER_ASSIST_BONUS;
						}
					}
				}

				CTFResetFlags();
				return false;
			}

			return false; /* its at home base already */
		}

		/* hey, its not home.  return it by teleporting it back */
		gi.bprintf(PRINT_HIGH, "%s returned the %s flag!\n",
				other->client->pers.netname, CTFTeamName(ctf_team));
		other->client->resp.score += CTF_RECOVERY_BONUS;
		other->client->resp.ctf_lastreturnedflag = level.time;
		gi.sound(ent, CHAN_RELIABLE + CHAN_NO_PHS_ADD + CHAN_VOICE,
				gi.soundindex("ctf/flagret.wav"), 1, ATTN_NONE, 0);

		/* CTFResetFlag will remove this entity!  We must return false */
		CTFResetFlag(ctf_team);
		return false;
	}

	/* hey, its not our flag, pick it up */
	gi.bprintf(PRINT_HIGH, "%s got the %s flag!\n",
			other->client->pers.netname, CTFTeamName(ctf_team));
	other->client->resp.score += CTF_FLAG_BONUS;

	other->client->pers.inventory[ITEM_INDEX(flag_item)] = 1;
	other->client->resp.ctf_flagsince = level.time;

	/* pick up the flag
	   if it's not a dropped flag,
	   we just make is disappear
	   if it's dropped, it will be
	   removed by the pickup caller */
	if (!(ent->spawnflags & DROPPED_ITEM))
	{
		ent->flags |= FL_RESPAWN;
		ent->svflags |= SVF_NOCLIENT;
		ent->solid = SOLID_NOT;
	}

	return true;
}

static void
CTFDropFlagTouch(edict_t *ent, edict_t *other,
		cplane_t *plane, csurface_t *surf)
{
	/* owner (who dropped us) can't touch for two secs */
	if ((other == ent->owner) &&
		(ent->nextthink - level.time > CTF_AUTO_FLAG_RETURN_TIMEOUT - 2))
	{
		return;
	}

	Touch_Item(ent, other, plane, surf);
}

static void
CTFDropFlagThink(edict_t *ent)
{
	/* auto return the flag
	   reset flag will remove
	   ourselves */
	if (strcmp(ent->classname, "item_flag_team1") == 0)
	{
		CTFResetFlag(CTF_TEAM1);
		gi.bprintf(PRINT_HIGH, "The %s flag has returned!\n",
				CTFTeamName(CTF_TEAM1));
	}
	else if (strcmp(ent->classname, "item_flag_team2") == 0)
	{
		CTFResetFlag(CTF_TEAM2);
		gi.bprintf(PRINT_HIGH, "The %s flag has returned!\n",
				CTFTeamName(CTF_TEAM2));
	}
}

/*
 * Called from PlayerDie, to drop
 * the flag from a dying player
 */
void
CTFDeadDropFlag(edict_t *self)
{
	edict_t *dropped = NULL;

	if (!flag1_item || !flag2_item)
	{
		return;
	}

	if (self->client->pers.inventory[ITEM_INDEX(flag1_item)])
	{
		dropped = Drop_Item(self, flag1_item);
		self->client->pers.inventory[ITEM_INDEX(flag1_item)] = 0;
		gi.bprintf(PRINT_HIGH, "%s lost the %s flag!\n",
				self->client->pers.netname, CTFTeamName(CTF_TEAM1));
	}
	else if (self->client->pers.inventory[ITEM_INDEX(flag2_item)])
	{
		dropped = Drop_Item(self, flag2_item);
		self->client->pers.inventory[ITEM_INDEX(flag2_item)] = 0;
		gi.bprintf(PRINT_HIGH, "%s lost the %s flag!\n",
				self->client->pers.netname, CTFTeamName(CTF_TEAM2));
	}

	if (dropped)
	{
		dropped->think = CTFDropFlagThink;
		dropped->nextthink = level.time + CTF_AUTO_FLAG_RETURN_TIMEOUT;
		dropped->touch = CTFDropFlagTouch;
	}
}

void
CTFDrop_Flag(edict_t *ent, gitem_t *item)
{
	if (rand() & 1)
	{
		gi.cprintf(ent, PRINT_HIGH, "Only lusers drop flags.\n");
	}
	else
	{
		gi.cprintf(ent, PRINT_HIGH, "Winners don't drop flags.\n");
	}
}

static void
CTFFlagThink(edict_t *ent)
{
	if (ent->solid != SOLID_NOT)
	{
		ent->s.frame = 173 + (((ent->s.frame - 173) + 1) % 16);
	}

	ent->nextthink = level.time + FRAMETIME;
}

void
CTFFlagSetup(edict_t *ent)
{
	trace_t tr;
	vec3_t dest;
	float *v;

	v = tv(-15, -15, -15);
	VectorCopy(v, ent->mins);
	v = tv(15, 15, 15);
	VectorCopy(v, ent->maxs);

	if (ent->model)
	{
		gi.setmodel(ent, ent->model);
	}
	else
	{
		gi.setmodel(ent, ent->item->world_model);
	}

	ent->solid = SOLID_TRIGGER;
	ent->movetype = MOVETYPE_TOSS;
	ent->touch = Touch_Item;

	v = tv(0, 0, -128);
	VectorAdd(ent->s.origin, v, dest);

	tr = gi.trace(ent->s.origin, ent->mins, ent->maxs, dest, ent, MASK_SOLID);

	if (tr.startsolid)
	{
		gi.dprintf("%s: %s startsolid at %s\n", __func__, ent->classname,
				vtos(ent->s.origin));
		G_FreeEdict(ent);
		return;
	}

	VectorCopy(tr.endpos, ent->s.origin);

	gi.linkentity(ent);

	ent->nextthink = level.time + FRAMETIME;
	ent->think = CTFFlagThink;
}

void
CTFEffects(edict_t *player)
{
	player->s.effects &= ~(EF_FLAG1 | EF_FLAG2);

	if (!flag1_item || !flag2_item)
	{
		return;
	}

	if (player->health > 0)
	{
		if (player->client->pers.inventory[ITEM_INDEX(flag1_item)])
		{
			player->s.effects |= EF_FLAG1;
		}

		if (player->client->pers.inventory[ITEM_INDEX(flag2_item)])
		{
			player->s.effects |= EF_FLAG2;
		}
	}

	if (player->client->pers.inventory[ITEM_INDEX(flag1_item)])
	{
		player->s.modelindex3 = gi.modelindex("players/male/flag1.md2");
	}
	else if (player->client->pers.inventory[ITEM_INDEX(flag2_item)])
	{
		player->s.modelindex3 = gi.modelindex("players/male/flag2.md2");
	}
	else
	{
		player->s.modelindex3 = 0;
	}
}

/*
 * Called when we enter the intermission
 */
void
CTFCalcScores(void)
{
	int i;

	ctfgame.total1 = ctfgame.total2 = 0;

	for (i = 0; i < maxclients->value; i++)
	{
		if (!g_edicts[i + 1].inuse)
		{
			continue;
		}

		if (game.clients[i].resp.ctf_team == CTF_TEAM1)
		{
			ctfgame.total1 += game.clients[i].resp.score;
		}
		else if (game.clients[i].resp.ctf_team == CTF_TEAM2)
		{
			ctfgame.total2 += game.clients[i].resp.score;
		}
	}
}

void
CTFID_f(edict_t *ent)
{
	if (ent->client->resp.id_state)
	{
		gi.cprintf(ent, PRINT_HIGH, "Disabling player identication display.\n");
		ent->client->resp.id_state = false;
	}
	else
	{
		gi.cprintf(ent, PRINT_HIGH, "Activating player identication display.\n");
		ent->client->resp.id_state = true;
	}
}

static void
CTFSetIDView(edict_t *ent)
{
	vec3_t forward, dir;
	trace_t tr;
	edict_t *who, *best;
	float bd = 0, d;
	int i;

	/* only check every few frames */
	if (level.time - ent->client->resp.lastidtime < 0.25)
	{
		return;
	}

	ent->client->resp.lastidtime = level.time;

	ent->client->ps.stats[STAT_CTF_ID_VIEW] = 0;
	ent->client->ps.stats[STAT_CTF_ID_VIEW_COLOR] = 0;

	AngleVectors(ent->client->v_angle, forward, NULL, NULL);
	VectorScale(forward, 1024, forward);
	VectorAdd(ent->s.origin, forward, forward);
	tr = gi.trace(ent->s.origin, NULL, NULL, forward, ent, MASK_SOLID);

	if ((tr.fraction < 1) && tr.ent && tr.ent->client)
	{
		ent->client->ps.stats[STAT_CTF_ID_VIEW] =
			CS_GENERAL + (tr.ent - g_edicts - 1);

		if (tr.ent->client->resp.ctf_team == CTF_TEAM1)
		{
			ent->client->ps.stats[STAT_CTF_ID_VIEW_COLOR] = imageindex_sbfctf1;
		}
		else if (tr.ent->client->resp.ctf_team == CTF_TEAM2)
		{
			ent->client->ps.stats[STAT_CTF_ID_VIEW_COLOR] = imageindex_sbfctf2;
		}

		return;
	}

	AngleVectors(ent->client->v_angle, forward, NULL, NULL);
	best = NULL;

	for (i = 1; i <= maxclients->value; i++)
	{
		who = g_edicts + i;

		if (!who->inuse || (who->solid == SOLID_NOT))
		{
			continue;
		}

		VectorSubtract(who->s.origin, ent->s.origin, dir);
		VectorNormalize(dir);
		d = DotProduct(forward, dir);

		if ((d > bd) && loc_CanSee(ent, who))
		{
			bd = d;
			best = who;
		}
	}

	if (bd > 0.90)
	{
		ent->client->ps.stats[STAT_CTF_ID_VIEW] =
			CS_GENERAL + (best - g_edicts - 1);

		if (best->client->resp.ctf_team == CTF_TEAM1)
		{
			ent->client->ps.stats[STAT_CTF_ID_VIEW_COLOR] = imageindex_sbfctf1;
		}
		else if (best->client->resp.ctf_team == CTF_TEAM2)
		{
			ent->client->ps.stats[STAT_CTF_ID_VIEW_COLOR] = imageindex_sbfctf2;
		}
	}
}

void
SetCTFStats(edict_t *ent)
{
	gitem_t *tech;
	int i;
	int p1, p2;
	edict_t *e;

	if (ctfgame.match > MATCH_NONE)
	{
		ent->client->ps.stats[STAT_CTF_MATCH] = CONFIG_CTF_MATCH;
	}
	else
	{
		ent->client->ps.stats[STAT_CTF_MATCH] = 0;
	}

	if (ctfgame.warnactive)
	{
		ent->client->ps.stats[STAT_CTF_TEAMINFO] = CONFIG_CTF_TEAMINFO;
	}
	else
	{
		ent->client->ps.stats[STAT_CTF_TEAMINFO] = 0;
	}

	/* ghosting */
	if (ent->client->resp.ghost)
	{
		ent->client->resp.ghost->score = ent->client->resp.score;
		strcpy(ent->client->resp.ghost->netname, ent->client->pers.netname);
		ent->client->resp.ghost->number = ent->s.number;
	}

	/* logo headers for the frag display */
	ent->client->ps.stats[STAT_CTF_TEAM1_HEADER] = imageindex_ctfsb1;
	ent->client->ps.stats[STAT_CTF_TEAM2_HEADER] = imageindex_ctfsb2;

	/* if during intermission, we must blink
	   the team header of the winning team */
	if (level.intermissiontime && (level.framenum & 8))   /* blink 1/8th second */
	{
		/* note that ctfgame.total[12] is
		   set when we go to intermission */
		if (ctfgame.team1 > ctfgame.team2)
		{
			ent->client->ps.stats[STAT_CTF_TEAM1_HEADER] = 0;
		}
		else if (ctfgame.team2 > ctfgame.team1)
		{
			ent->client->ps.stats[STAT_CTF_TEAM2_HEADER] = 0;
		}
		else if (ctfgame.total1 > ctfgame.total2) /* frag tie breaker */
		{
			ent->client->ps.stats[STAT_CTF_TEAM1_HEADER] = 0;
		}
		else if (ctfgame.total2 > ctfgame.total1)
		{
			ent->client->ps.stats[STAT_CTF_TEAM2_HEADER] = 0;
		}
		else /* tie game! */
		{
			ent->client->ps.stats[STAT_CTF_TEAM1_HEADER] = 0;
			ent->client->ps.stats[STAT_CTF_TEAM2_HEADER] = 0;
		}
	}

	/* tech icon */
	i = 0;
	ent->client->ps.stats[STAT_CTF_TECH] = 0;

	while (tnames[i])
	{
		if (((tech = FindItemByClassname(tnames[i])) != NULL) &&
			ent->client->pers.inventory[ITEM_INDEX(tech)])
		{
			ent->client->ps.stats[STAT_CTF_TECH] = gi.imageindex(tech->icon);
			break;
		}

		i++;
	}

	/* figure out what icon to display for team logos
	   three states:
	     flag at base
	     flag taken
	     flag dropped */
	p1 = imageindex_i_ctf1;
	e = G_Find(NULL, FOFS(classname), "item_flag_team1");

	if (e != NULL)
	{
		if (e->solid == SOLID_NOT)
		{
			int i;

			/* not at base
			  check if on player */
			p1 = imageindex_i_ctf1d; /* default to dropped */

			for (i = 1; i <= maxclients->value; i++)
			{
				if (g_edicts[i].inuse &&
					g_edicts[i].client->pers.inventory[ITEM_INDEX(flag1_item)])
				{
					/* enemy has it */
					p1 = imageindex_i_ctf1t;
					break;
				}
			}
		}
		else if (e->spawnflags & DROPPED_ITEM)
		{
			p1 = imageindex_i_ctf1d; /* must be dropped */
		}
	}

	p2 = imageindex_i_ctf2;
	e = G_Find(NULL, FOFS(classname), "item_flag_team2");

	if (e != NULL)
	{
		if (e->solid == SOLID_NOT)
		{
			int i;

			/* not at base
			   check if on player */
			p2 = imageindex_i_ctf2d; /* default to dropped */

			for (i = 1; i <= maxclients->value; i++)
			{
				if (g_edicts[i].inuse &&
					g_edicts[i].client->pers.inventory[ITEM_INDEX(flag2_item)])
				{
					/* enemy has it */
					p2 = imageindex_i_ctf2t;
					break;
				}
			}
		}
		else if (e->spawnflags & DROPPED_ITEM)
		{
			p2 = imageindex_i_ctf2d; /* must be dropped */
		}
	}

	ent->client->ps.stats[STAT_CTF_TEAM1_PIC] = p1;
	ent->client->ps.stats[STAT_CTF_TEAM2_PIC] = p2;

	if (ctfgame.last_flag_capture &&
		(level.time - ctfgame.last_flag_capture < 5))
	{
		if (ctfgame.last_capture_team == CTF_TEAM1)
		{
			if (level.framenum & 8)
			{
				ent->client->ps.stats[STAT_CTF_TEAM1_PIC] = p1;
			}
			else
			{
				ent->client->ps.stats[STAT_CTF_TEAM1_PIC] = 0;
			}
		}
		else
		if (level.framenum & 8)
		{
			ent->client->ps.stats[STAT_CTF_TEAM2_PIC] = p2;
		}
		else
		{
			ent->client->ps.stats[STAT_CTF_TEAM2_PIC] = 0;
		}
	}

	ent->client->ps.stats[STAT_CTF_TEAM1_CAPS] = ctfgame.team1;
	ent->client->ps.stats[STAT_CTF_TEAM2_CAPS] = ctfgame.team2;

	ent->client->ps.stats[STAT_CTF_FLAG_PIC] = 0;

	if ((ent->client->resp.ctf_team == CTF_TEAM1) &&
		ent->client->pers.inventory[ITEM_INDEX(flag2_item)] &&
		(level.framenum & 8))
	{
		ent->client->ps.stats[STAT_CTF_FLAG_PIC] = imageindex_i_ctf2;
	}

	else if ((ent->client->resp.ctf_team == CTF_TEAM2) &&
			 ent->client->pers.inventory[ITEM_INDEX(flag1_item)] &&
			 (level.framenum & 8))
	{
		ent->client->ps.stats[STAT_CTF_FLAG_PIC] = imageindex_i_ctf1;
	}

	ent->client->ps.stats[STAT_CTF_JOINED_TEAM1_PIC] = 0;
	ent->client->ps.stats[STAT_CTF_JOINED_TEAM2_PIC] = 0;

	if (ent->client->resp.ctf_team == CTF_TEAM1)
	{
		ent->client->ps.stats[STAT_CTF_JOINED_TEAM1_PIC] = imageindex_i_ctfj;
	}
	else if (ent->client->resp.ctf_team == CTF_TEAM2)
	{
		ent->client->ps.stats[STAT_CTF_JOINED_TEAM2_PIC] = imageindex_i_ctfj;
	}

	if (ent->client->resp.id_state)
	{
		CTFSetIDView(ent);
	}
	else
	{
		ent->client->ps.stats[STAT_CTF_ID_VIEW] = 0;
		ent->client->ps.stats[STAT_CTF_ID_VIEW_COLOR] = 0;
	}
}

/*------------------------------------------------------------------------*/

/*
 * QUAKED info_player_team1 (1 0 0) (-16 -16 -24) (16 16 32)
 * potential team1 spawning position for ctf games
 */
void
SP_info_player_team1(edict_t *self)
{
	DynamicResetSpawnModels(self);
}

/*
 * QUAKED info_player_team2 (0 0 1) (-16 -16 -24) (16 16 32)
 * potential team2 spawning position for ctf games
 */
void
SP_info_player_team2(edict_t *self)
{
	DynamicResetSpawnModels(self);
}

/*------------------------------------------------------------------------*/
/* GRAPPLE																  */
/*------------------------------------------------------------------------*/

/* ent is player */
void
CTFPlayerResetGrapple(edict_t *ent)
{
	if (ent->client && ent->client->ctf_grapple)
	{
		CTFResetGrapple(ent->client->ctf_grapple);
	}
}

/* self is grapple, not player */
void
CTFResetGrapple(edict_t *self)
{
	if (self->owner->client->ctf_grapple)
	{
		float volume = 1.0;
		gclient_t *cl;

		if (self->owner->client->silencer_shots)
		{
			volume = 0.2;
		}

		gi.sound(self->owner, CHAN_RELIABLE + CHAN_WEAPON,
				gi.soundindex("weapons/grapple/grreset.wav"), volume, ATTN_NORM, 0);
		cl = self->owner->client;
		cl->ctf_grapple = NULL;
		cl->ctf_grapplereleasetime = level.time;
		cl->ctf_grapplestate = CTF_GRAPPLE_STATE_FLY; /* we're firing, not on hook */
		cl->ps.pmove.pm_flags &= ~PMF_NO_PREDICTION;
		G_FreeEdict(self);
	}
}

static void
CTFGrappleTouch(edict_t *self, edict_t *other, cplane_t *plane, csurface_t *surf)
{
	float volume = 1.0;

	if (other == self->owner)
	{
		return;
	}

	if (self->owner->client->ctf_grapplestate != CTF_GRAPPLE_STATE_FLY)
	{
		return;
	}

	if (surf && (surf->flags & SURF_SKY))
	{
		CTFResetGrapple(self);
		return;
	}

	VectorCopy(vec3_origin, self->velocity);

	PlayerNoise(self->owner, self->s.origin, PNOISE_IMPACT);

	if (other->takedamage)
	{
		T_Damage(other, self, self->owner, self->velocity, self->s.origin,
				plane->normal, self->dmg, 1, 0, MOD_GRAPPLE);
		CTFResetGrapple(self);
		return;
	}

	self->owner->client->ctf_grapplestate = CTF_GRAPPLE_STATE_PULL; /* we're on hook */
	self->enemy = other;

	self->solid = SOLID_NOT;

	if (self->owner->client->silencer_shots)
	{
		volume = 0.2;
	}

	gi.sound(self->owner, CHAN_RELIABLE + CHAN_WEAPON,
			gi.soundindex("weapons/grapple/grpull.wav"), volume, ATTN_NORM, 0);
	gi.sound(self, CHAN_WEAPON, gi.soundindex(
					"weapons/grapple/grhit.wav"), volume, ATTN_NORM, 0);

	gi.WriteByte(svc_temp_entity);
	gi.WriteByte(TE_SPARKS);
	gi.WritePosition(self->s.origin);

	if (!plane)
	{
		gi.WriteDir(vec3_origin);
	}
	else
	{
		gi.WriteDir(plane->normal);
	}

	gi.multicast(self->s.origin, MULTICAST_PVS);
}

/*
 * Draw beam between grapple and self
 */
static void
CTFGrappleDrawCable(edict_t *self)
{
	vec3_t offset, start, end, f, r;
	vec3_t dir;
	float distance;

	AngleVectors(self->owner->client->v_angle, f, r, NULL);
	VectorSet(offset, 16, 16, self->owner->viewheight - 8);
	P_ProjectSource(self->owner, offset, f, r, start);

	VectorSubtract(start, self->owner->s.origin, offset);

	VectorSubtract(start, self->s.origin, dir);
	distance = VectorLength(dir);

	/* don't draw cable if close */
	if (distance < 64)
	{
		return;
	}

	/* adjust start for beam origin being in middle of a segment */
	VectorCopy(self->s.origin, end);

	gi.WriteByte(svc_temp_entity);
	gi.WriteByte(TE_GRAPPLE_CABLE);
	gi.WriteShort(self->owner - g_edicts);
	gi.WritePosition(self->owner->s.origin);
	gi.WritePosition(end);
	gi.WritePosition(offset);
	gi.multicast(self->s.origin, MULTICAST_PVS);
}

/*
 * pull the player toward the grapple
 */
void
CTFGrapplePull(edict_t *self)
{
	vec3_t hookdir, v;
	float vlen;

	if ((strcmp(self->owner->client->pers.weapon->classname,
				 "weapon_grapple") == 0) &&
		!self->owner->client->newweapon &&
		(self->owner->client->weaponstate != WEAPON_FIRING) &&
		(self->owner->client->weaponstate != WEAPON_ACTIVATING))
	{
		CTFResetGrapple(self);
		return;
	}

	if (self->enemy)
	{
		if (self->enemy->solid == SOLID_NOT)
		{
			CTFResetGrapple(self);
			return;
		}

		if (self->enemy->solid == SOLID_BBOX)
		{
			VectorScale(self->enemy->size, 0.5, v);
			VectorAdd(v, self->enemy->s.origin, v);
			VectorAdd(v, self->enemy->mins, self->s.origin);
			gi.linkentity(self);
		}
		else
		{
			VectorCopy(self->enemy->velocity, self->velocity);
		}

		if (self->enemy->takedamage &&
			!CheckTeamDamage(self->enemy, self->owner))
		{
			float volume = 1.0;

			if (self->owner->client->silencer_shots)
			{
				volume = 0.2;
			}

			T_Damage(self->enemy, self, self->owner, self->velocity,
					self->s.origin, vec3_origin, 1, 1, 0, MOD_GRAPPLE);
			gi.sound(self, CHAN_WEAPON,
					gi.soundindex("weapons/grapple/grhurt.wav"), volume, ATTN_NORM, 0);
		}

		if (self->enemy->deadflag) /* he died */
		{
			CTFResetGrapple(self);
			return;
		}
	}

	CTFGrappleDrawCable(self);

	if (self->owner->client->ctf_grapplestate > CTF_GRAPPLE_STATE_FLY)
	{
		/* pull player toward grapple
		   this causes icky stuff with prediction, we need to extend/
		   the prediction layer to include two new fields in the player/
		   move stuff: a point and a velocity. The client should add
		   that velociy in the direction of the point */
		vec3_t forward, up;

		AngleVectors(self->owner->client->v_angle, forward, NULL, up);
		VectorCopy(self->owner->s.origin, v);
		v[2] += self->owner->viewheight;
		VectorSubtract(self->s.origin, v, hookdir);

		vlen = VectorLength(hookdir);

		if ((self->owner->client->ctf_grapplestate ==
			 CTF_GRAPPLE_STATE_PULL) &&
			(vlen < 64))
		{
			float volume = 1.0;

			if (self->owner->client->silencer_shots)
			{
				volume = 0.2;
			}

			self->owner->client->ps.pmove.pm_flags |= PMF_NO_PREDICTION;
			gi.sound(self->owner, CHAN_RELIABLE + CHAN_WEAPON,
					gi.soundindex("weapons/grapple/grhang.wav"), volume, ATTN_NORM, 0);
			self->owner->client->ctf_grapplestate = CTF_GRAPPLE_STATE_HANG;
		}

		VectorNormalize(hookdir);
		VectorScale(hookdir, CTF_GRAPPLE_PULL_SPEED, hookdir);
		VectorCopy(hookdir, self->owner->velocity);
		SV_AddGravity(self->owner);
	}
}

static void
CTFFireGrapple(edict_t *self, vec3_t start, vec3_t dir,
		int damage, int speed, int effect)
{
	edict_t *grapple;
	trace_t tr;

	VectorNormalize(dir);

	grapple = G_Spawn();
	VectorCopy(start, grapple->s.origin);
	VectorCopy(start, grapple->s.old_origin);
	vectoangles(dir, grapple->s.angles);
	VectorScale(dir, speed, grapple->velocity);
	grapple->movetype = MOVETYPE_FLYMISSILE;
	grapple->clipmask = MASK_SHOT;
	grapple->solid = SOLID_BBOX;
	grapple->s.effects |= effect;
	VectorClear(grapple->mins);
	VectorClear(grapple->maxs);
	grapple->s.modelindex = gi.modelindex("models/weapons/grapple/hook/tris.md2");
	grapple->owner = self;
	grapple->touch = CTFGrappleTouch;
	grapple->dmg = damage;
	self->client->ctf_grapple = grapple;
	self->client->ctf_grapplestate = CTF_GRAPPLE_STATE_FLY; /* we're firing, not on hook */
	gi.linkentity(grapple);

	tr = gi.trace(self->s.origin, NULL, NULL, grapple->s.origin, grapple, MASK_SHOT);

	if (tr.fraction < 1.0)
	{
		VectorMA(grapple->s.origin, -10, dir, grapple->s.origin);
		grapple->touch(grapple, tr.ent, NULL, NULL);
	}
}

static void
CTFGrappleFire(edict_t *ent, vec3_t g_offset, int damage, int effect)
{
	vec3_t forward, right;
	vec3_t start;
	vec3_t offset;
	float volume = 1.0;

	if (ent->client->ctf_grapplestate > CTF_GRAPPLE_STATE_FLY)
	{
		return; /* it's already out */
	}

	AngleVectors(ent->client->v_angle, forward, right, NULL);
	VectorSet(offset, 24, 8, ent->viewheight - 8 + 2);
	VectorAdd(offset, g_offset, offset);
	P_ProjectSource(ent, offset, forward, right, start);

	VectorScale(forward, -2, ent->client->kick_origin);
	ent->client->kick_angles[0] = -1;

	if (ent->client->silencer_shots)
	{
		volume = 0.2;
	}

	gi.sound(ent, CHAN_RELIABLE + CHAN_WEAPON,
			gi.soundindex("weapons/grapple/grfire.wav"), volume, ATTN_NORM, 0);
	CTFFireGrapple(ent, start, forward, damage, CTF_GRAPPLE_SPEED, effect);
	PlayerNoise(ent, start, PNOISE_WEAPON);
}

static void
CTFWeapon_Grapple_Fire(edict_t *ent)
{
	int damage;

	damage = 10;
	CTFGrappleFire(ent, vec3_origin, damage, 0);
	ent->client->ps.gunframe++;
}

void
CTFWeapon_Grapple(edict_t *ent)
{
	static int pause_frames[] = {10, 18, 27, 0};
	static int fire_frames[] = {6, 0};
	int prevstate;

	/* if the the attack button is still down, stay in the firing frame */
	if ((ent->client->buttons & BUTTON_ATTACK) &&
		(ent->client->weaponstate == WEAPON_FIRING) &&
		ent->client->ctf_grapple)
	{
		ent->client->ps.gunframe = 9;
	}

	if (!(ent->client->buttons & BUTTON_ATTACK) &&
		ent->client->ctf_grapple)
	{
		CTFResetGrapple(ent->client->ctf_grapple);

		if (ent->client->weaponstate == WEAPON_FIRING)
		{
			ent->client->weaponstate = WEAPON_READY;
		}
	}

	if (ent->client->newweapon &&
		(ent->client->ctf_grapplestate > CTF_GRAPPLE_STATE_FLY) &&
		(ent->client->weaponstate == WEAPON_FIRING))
	{
		/* he wants to change weapons while grappled */
		ent->client->weaponstate = WEAPON_DROPPING;
		ent->client->ps.gunframe = 32;
	}

	prevstate = ent->client->weaponstate;
	Weapon_Generic(ent, 5, 9, 31, 36, pause_frames, fire_frames,
			CTFWeapon_Grapple_Fire);

	/* if we just switched back to grapple, immediately go to fire frame */
	if ((prevstate == WEAPON_ACTIVATING) &&
		(ent->client->weaponstate == WEAPON_READY) &&
		(ent->client->ctf_grapplestate > CTF_GRAPPLE_STATE_FLY))
	{
		if (!(ent->client->buttons & BUTTON_ATTACK))
		{
			ent->client->ps.gunframe = 9;
		}
		else
		{
			ent->client->ps.gunframe = 5;
		}

		ent->client->weaponstate = WEAPON_FIRING;
	}
}

void
CTFTeam_f(edict_t *ent)
{
	char *t, *s;
	int desired_team;

	t = gi.args();

	if (!*t)
	{
		gi.cprintf(ent, PRINT_HIGH, "You are on the %s team.\n",
				CTFTeamName(ent->client->resp.ctf_team));
		return;
	}

	if (ctfgame.match > MATCH_SETUP)
	{
		gi.cprintf(ent, PRINT_HIGH, "Can't change teams in a match.\n");
		return;
	}

	if (Q_stricmp(t, "red") == 0)
	{
		desired_team = CTF_TEAM1;
	}
	else if (Q_stricmp(t, "blue") == 0)
	{
		desired_team = CTF_TEAM2;
	}
	else
	{
		gi.cprintf(ent, PRINT_HIGH, "Unknown team %s.\n", t);
		return;
	}

	if (ent->client->resp.ctf_team == desired_team)
	{
		gi.cprintf(ent, PRINT_HIGH, "You are already on the %s team.\n",
				CTFTeamName(ent->client->resp.ctf_team));
		return;
	}

	ent->svflags = 0;
	ent->flags &= ~FL_GODMODE;
	ent->client->resp.ctf_team = desired_team;
	ent->client->resp.ctf_state = 0;
	s = Info_ValueForKey(ent->client->pers.userinfo, "skin");
	CTFAssignSkin(ent, s);

	if (ent->solid == SOLID_NOT) /* spectator */
	{
		PutClientInServer(ent);

		/* add a teleportation effect */
		ent->s.event = EV_PLAYER_TELEPORT;

		/* hold in place briefly */
		ent->client->ps.pmove.pm_flags = PMF_TIME_TELEPORT;
		ent->client->ps.pmove.pm_time = 14;
		gi.bprintf(PRINT_HIGH, "%s joined the %s team.\n",
				ent->client->pers.netname, CTFTeamName(desired_team));
		return;
	}

	ent->health = 0;
	player_die(ent, ent, ent, 100000, vec3_origin);

	/* don't even bother waiting for death frames */
	ent->deadflag = DEAD_DEAD;
	respawn(ent);

	ent->client->resp.score = 0;

	gi.bprintf(PRINT_HIGH, "%s changed to the %s team.\n",
			ent->client->pers.netname, CTFTeamName(desired_team));
}

void
CTFScoreboardMessage(edict_t *ent, edict_t *killer)
{
	char entry[1024];
	char string[1400];
	size_t len;
	int i, j, k, n;
	int sorted[2][MAX_CLIENTS];
	int sortedscores[2][MAX_CLIENTS];
	int score, total[2], totalscore[2];
	int last[2];
	gclient_t *cl;
	edict_t *cl_ent;
	int team;
	int maxsize = 1000;

	/* sort the clients by team and score */
	total[0] = total[1] = 0;
	last[0] = last[1] = 0;
	totalscore[0] = totalscore[1] = 0;

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

		if (!cl_ent->inuse)
		{
			continue;
		}

		if (game.clients[i].resp.ctf_team == CTF_TEAM1)
		{
			team = 0;
		}
		else if (game.clients[i].resp.ctf_team == CTF_TEAM2)
		{
			team = 1;
		}
		else
		{
			continue; /* unknown team? */
		}

		score = game.clients[i].resp.score;

		for (j = 0; j < total[team]; j++)
		{
			if (score > sortedscores[team][j])
			{
				break;
			}
		}

		for (k = total[team]; k > j; k--)
		{
			sorted[team][k] = sorted[team][k - 1];
			sortedscores[team][k] = sortedscores[team][k - 1];
		}

		sorted[team][j] = i;
		sortedscores[team][j] = score;
		totalscore[team] += score;
		total[team]++;
	}

	/* print level name and exit rules
	   add the clients in sorted order */
	*string = 0;
	len = 0;

	/* team one */
	sprintf(string, "if 24 xv 8 yv 8 pic 24 endif "
					"xv 40 yv 28 string \"%4d/%-3d\" "
					"xv 98 yv 12 num 2 18 "
					"if 25 xv 168 yv 8 pic 25 endif "
					"xv 200 yv 28 string \"%4d/%-3d\" "
					"xv 256 yv 12 num 2 20 ",
			totalscore[0], total[0],
			totalscore[1], total[1]);
	len = strlen(string);

	for (i = 0; i < 16; i++)
	{
		if ((i >= total[0]) && (i >= total[1]))
		{
			break; /* we're done */
		}

		*entry = 0;

		/* left side */
		if (i < total[0])
		{
			cl = &game.clients[sorted[0][i]];
			cl_ent = g_edicts + 1 + sorted[0][i];

			sprintf(entry + strlen(entry), "ctf 0 %d %d %d %d ",
					42 + i * 8, sorted[0][i], cl->resp.score,
					cl->ping > 999 ? 999 : cl->ping);

			if (cl_ent->client->pers.inventory[ITEM_INDEX(flag2_item)])
			{
				sprintf(entry + strlen(entry), "xv 56 yv %d picn sbfctf2 ",
						42 + i * 8);
			}

			if (maxsize - len > strlen(entry))
			{
				strcat(string, entry);
				len = strlen(string);
				last[0] = i;
			}
		}

		/* right side */
		if (i < total[1])
		{
			cl = &game.clients[sorted[1][i]];
			cl_ent = g_edicts + 1 + sorted[1][i];

			sprintf(entry + strlen(entry), "ctf 160 %d %d %d %d ",
					42 + i * 8, sorted[1][i], cl->resp.score,
					cl->ping > 999 ? 999 : cl->ping);

			if (cl_ent->client->pers.inventory[ITEM_INDEX(flag1_item)])
			{
				sprintf(entry + strlen(entry), "xv 216 yv %d picn sbfctf1 ",
						42 + i * 8);
			}

			if (maxsize - len > strlen(entry))
			{
				strcat(string, entry);
				len = strlen(string);
				last[1] = i;
			}
		}
	}

	/* put in spectators if we have enough room */
	if (last[0] > last[1])
	{
		j = last[0];
	}
	else
	{
		j = last[1];
	}

	j = (j + 2) * 8 + 42;

	k = n = 0;

	if (maxsize - len > 50)
	{
		for (i = 0; i < maxclients->value; i++)
		{
			cl_ent = g_edicts + 1 + i;
			cl = &game.clients[i];

			if (!cl_ent->inuse ||
				(cl_ent->solid != SOLID_NOT) ||
				(cl_ent->client->resp.ctf_team != CTF_NOTEAM))
			{
				continue;
			}

			if (!k)
			{
				k = 1;
				sprintf(entry, "xv 0 yv %d string2 \"Spectators\" ", j);
				strcat(string, entry);
				len = strlen(string);
				j += 8;
			}

			sprintf(entry + strlen(entry),
					"ctf %d %d %d %d %d ",
					(n & 1) ? 160 : 0, /* x */
					j, /* y */
					i, /* playernum */
					cl->resp.score,
					cl->ping > 999 ? 999 : cl->ping);

			if (maxsize - len > strlen(entry))
			{
				strcat(string, entry);
				len = strlen(string);
			}

			if (n & 1)
			{
				j += 8;
			}

			n++;
		}
	}

	if (total[0] - last[0] > 1) /* couldn't fit everyone */
	{
		sprintf(string + strlen(string), "xv 8 yv %d string \"..and %d more\" ",
				42 + (last[0] + 1) * 8, total[0] - last[0] - 1);
	}

	if (total[1] - last[1] > 1) /* couldn't fit everyone */
	{
		sprintf(string + strlen(
						string), "xv 168 yv %d string \"..and %d more\" ",
				42 + (last[1] + 1) * 8, total[1] - last[1] - 1);
	}

	gi.WriteByte(svc_layout);
	gi.WriteString(string);
}

/*------------------------------------------------------------------------*/
/* TECH																	  */
/*------------------------------------------------------------------------*/

static void
CTFHasTech(edict_t *who)
{
	if (level.time - who->client->ctf_lasttechmsg > 2)
	{
		gi.centerprintf(who, "You already have a TECH powerup.");
		who->client->ctf_lasttechmsg = level.time;
	}
}

gitem_t *
CTFWhat_Tech(edict_t *ent)
{
	gitem_t *tech;
	int i;

	i = 0;

	while (tnames[i])
	{
		if (((tech = FindItemByClassname(tnames[i])) != NULL) &&
			ent->client->pers.inventory[ITEM_INDEX(tech)])
		{
			return tech;
		}

		i++;
	}

	return NULL;
}

qboolean
CTFPickup_Tech(edict_t *ent, edict_t *other)
{
	gitem_t *tech;
	int i;

	i = 0;

	while (tnames[i])
	{
		if (((tech = FindItemByClassname(tnames[i])) != NULL) &&
			other->client->pers.inventory[ITEM_INDEX(tech)])
		{
			CTFHasTech(other);
			return false; /* has this one */
		}

		i++;
	}

	/* client only gets one tech */
	other->client->pers.inventory[ITEM_INDEX(ent->item)]++;
	other->client->ctf_regentime = level.time;
	return true;
}

static void SpawnTech(gitem_t *item, edict_t *spot);

static edict_t *
FindTechSpawn(void)
{
	edict_t *spot = NULL;
	int i = rand() % 16;

	while (i--)
	{
		spot = G_Find(spot, FOFS(classname), "info_player_deathmatch");
	}

	if (!spot)
	{
		spot = G_Find(spot, FOFS(classname), "info_player_deathmatch");
	}

	return spot;
}

static void
TechThink(edict_t *tech)
{
	edict_t *spot;

	if ((spot = FindTechSpawn()) != NULL)
	{
		SpawnTech(tech->item, spot);
		G_FreeEdict(tech);
	}
	else
	{
		tech->nextthink = level.time + CTF_TECH_TIMEOUT;
		tech->think = TechThink;
	}
}

void
CTFDrop_Tech(edict_t *ent, gitem_t *item)
{
	edict_t *tech;

	tech = Drop_Item(ent, item);
	tech->nextthink = level.time + CTF_TECH_TIMEOUT;
	tech->think = TechThink;
	ent->client->pers.inventory[ITEM_INDEX(item)] = 0;
}

void
CTFDeadDropTech(edict_t *ent)
{
	gitem_t *tech;
	edict_t *dropped;
	int i;

	i = 0;

	while (tnames[i])
	{
		if (((tech = FindItemByClassname(tnames[i])) != NULL) &&
			ent->client->pers.inventory[ITEM_INDEX(tech)])
		{
			dropped = Drop_Item(ent, tech);

			/* hack the velocity to make it bounce random */
			dropped->velocity[0] = (rand() % 600) - 300;
			dropped->velocity[1] = (rand() % 600) - 300;
			dropped->nextthink = level.time + CTF_TECH_TIMEOUT;
			dropped->think = TechThink;
			dropped->owner = NULL;
			ent->client->pers.inventory[ITEM_INDEX(tech)] = 0;
		}

		i++;
	}
}

static void
SpawnTech(gitem_t *item, edict_t *spot)
{
	edict_t *ent;
	vec3_t forward, right;
	vec3_t angles;

	ent = G_Spawn();

	ent->classname = item->classname;
	ent->item = item;
	ent->spawnflags = DROPPED_ITEM;
	ent->s.effects = item->world_model_flags;
	ent->s.renderfx = RF_GLOW;
	VectorSet(ent->mins, -15, -15, -15);
	VectorSet(ent->maxs, 15, 15, 15);
	gi.setmodel(ent, ent->item->world_model);
	ent->solid = SOLID_TRIGGER;
	ent->movetype = MOVETYPE_TOSS;
	ent->touch = Touch_Item;
	ent->owner = ent;

	angles[0] = 0;
	angles[1] = rand() % 360;
	angles[2] = 0;

	AngleVectors(angles, forward, right, NULL);
	VectorCopy(spot->s.origin, ent->s.origin);
	ent->s.origin[2] += 16;
	VectorScale(forward, 100, ent->velocity);
	ent->velocity[2] = 300;

	ent->nextthink = level.time + CTF_TECH_TIMEOUT;
	ent->think = TechThink;

	gi.linkentity(ent);
}

static void
SpawnTechs(edict_t *ent)
{
	gitem_t *tech;
	edict_t *spot;
	int i;

	i = 0;

	while (tnames[i])
	{
		if (((tech = FindItemByClassname(tnames[i])) != NULL) &&
			((spot = FindTechSpawn()) != NULL))
		{
			SpawnTech(tech, spot);
		}

		i++;
	}

	if (ent)
	{
		G_FreeEdict(ent);
	}
}

/*
 * frees the passed edict!
 */
void
CTFRespawnTech(edict_t *ent)
{
	edict_t *spot;

	if ((spot = FindTechSpawn()) != NULL)
	{
		SpawnTech(ent->item, spot);
	}

	G_FreeEdict(ent);
}

void
CTFSetupTechSpawn(void)
{
	edict_t *ent;

	if (((int)dmflags->value & DF_CTF_NO_TECH))
	{
		return;
	}

	ent = G_Spawn();
	ent->nextthink = level.time + 2;
	ent->think = SpawnTechs;
}

void
CTFResetTech(void)
{
	edict_t *ent;
	int i;

	for (ent = g_edicts + 1, i = 1; i < globals.num_edicts; i++, ent++)
	{
		if (ent->inuse)
		{
			if (ent->item && (ent->item->flags & IT_TECH))
			{
				G_FreeEdict(ent);
			}
		}
	}

	SpawnTechs(NULL);
}

int
CTFApplyResistance(edict_t *ent, int dmg)
{
	static gitem_t *tech = NULL;
	float volume = 1.0;

	if (ent->client && ent->client->silencer_shots)
	{
		volume = 0.2;
	}

	if (!tech)
	{
		tech = FindItemByClassname("item_tech1");
	}

	if (dmg && tech && ent->client &&
		ent->client->pers.inventory[ITEM_INDEX(tech)])
	{
		/* make noise */
		gi.sound(ent, CHAN_VOICE, gi.soundindex(
						"ctf/tech1.wav"), volume, ATTN_NORM, 0);
		return dmg / 2;
	}

	return dmg;
}

int
CTFApplyStrength(edict_t *ent, int dmg)
{
	static gitem_t *tech = NULL;

	if (!tech)
	{
		tech = FindItemByClassname("item_tech2");
	}

	if (dmg && tech && ent->client &&
		ent->client->pers.inventory[ITEM_INDEX(tech)])
	{
		return dmg * 2;
	}

	return dmg;
}

qboolean
CTFApplyStrengthSound(edict_t *ent)
{
	static gitem_t *tech = NULL;
	float volume = 1.0;

	if (ent->client && ent->client->silencer_shots)
	{
		volume = 0.2;
	}

	if (!tech)
	{
		tech = FindItemByClassname("item_tech2");
	}

	if (tech && ent->client &&
		ent->client->pers.inventory[ITEM_INDEX(tech)])
	{
		if (ent->client->ctf_techsndtime < level.time)
		{
			ent->client->ctf_techsndtime = level.time + 1;

			if (ent->client->quad_framenum > level.framenum)
			{
				gi.sound(ent, CHAN_VOICE, gi.soundindex(
								"ctf/tech2x.wav"), volume, ATTN_NORM, 0);
			}
			else
			{
				gi.sound(ent, CHAN_VOICE, gi.soundindex(
								"ctf/tech2.wav"), volume, ATTN_NORM, 0);
			}
		}

		return true;
	}

	return false;
}

qboolean
CTFApplyHaste(edict_t *ent)
{
	static gitem_t *tech = NULL;

	if (!tech)
	{
		tech = FindItemByClassname("item_tech3");
	}

	if (tech && ent->client &&
		ent->client->pers.inventory[ITEM_INDEX(tech)])
	{
		return true;
	}

	return false;
}

void
CTFApplyHasteSound(edict_t *ent)
{
	static gitem_t *tech = NULL;
	float volume = 1.0;

	if (ent->client && ent->client->silencer_shots)
	{
		volume = 0.2;
	}

	if (!tech)
	{
		tech = FindItemByClassname("item_tech3");
	}

	if (tech && ent->client &&
		ent->client->pers.inventory[ITEM_INDEX(tech)] &&
		(ent->client->ctf_techsndtime < level.time))
	{
		ent->client->ctf_techsndtime = level.time + 1;
		gi.sound(ent, CHAN_VOICE, gi.soundindex(
						"ctf/tech3.wav"), volume, ATTN_NORM, 0);
	}
}

void
CTFApplyRegeneration(edict_t *ent)
{
	static gitem_t *tech = NULL;
	qboolean noise = false;
	gclient_t *client;
	int index;
	float volume = 1.0;

	client = ent->client;

	if (!client)
	{
		return;
	}

	if (ent->client->silencer_shots)
	{
		volume = 0.2;
	}

	if (!tech)
	{
		tech = FindItemByClassname("item_tech4");
	}

	if (tech && client->pers.inventory[ITEM_INDEX(tech)])
	{
		if (client->ctf_regentime < level.time)
		{
			client->ctf_regentime = level.time;

			if (ent->health < 150)
			{
				ent->health += 5;

				if (ent->health > 150)
				{
					ent->health = 150;
				}

				client->ctf_regentime += 0.5;
				noise = true;
			}

			index = ArmorIndex(ent);

			if (index && (client->pers.inventory[index] < 150))
			{
				client->pers.inventory[index] += 5;

				if (client->pers.inventory[index] > 150)
				{
					client->pers.inventory[index] = 150;
				}

				client->ctf_regentime += 0.5;
				noise = true;
			}
		}

		if (noise && (ent->client->ctf_techsndtime < level.time))
		{
			ent->client->ctf_techsndtime = level.time + 1;
			gi.sound(ent, CHAN_VOICE, gi.soundindex(
							"ctf/tech4.wav"), volume, ATTN_NORM, 0);
		}
	}
}

qboolean
CTFHasRegeneration(edict_t *ent)
{
	static gitem_t *tech = NULL;

	if (!tech)
	{
		tech = FindItemByClassname("item_tech4");
	}

	if (tech && ent->client &&
		ent->client->pers.inventory[ITEM_INDEX(tech)])
	{
		return true;
	}

	return false;
}

/*
 * ======================================================================
 *
 * SAY_TEAM
 *
 * ======================================================================
 */

/*
 * This array is in 'importance order',
 * it indicates what items are more
 * important when reporting their names.
 */
struct
{
	char *classname;
	int priority;
} loc_names[] = {
	{"item_flag_team1", 1},
	{"item_flag_team2", 1},
	{"item_quad", 2},
	{"item_invulnerability", 2},
	{"weapon_bfg", 3},
	{"weapon_railgun", 4},
	{"weapon_rocketlauncher", 4},
	{"weapon_hyperblaster", 4},
	{"weapon_chaingun", 4},
	{"weapon_grenadelauncher", 4},
	{"weapon_machinegun", 4},
	{"weapon_supershotgun", 4},
	{"weapon_shotgun", 4},
	{"item_power_screen", 5},
	{"item_power_shield", 5},
	{"item_armor_body", 6},
	{"item_armor_combat", 6},
	{"item_armor_jacket", 6},
	{"item_silencer", 7},
	{"item_breather", 7},
	{"item_enviro", 7},
	{"item_adrenaline", 7},
	{"item_bandolier", 8},
	{"item_pack", 8},
	{NULL, 0}
};

static void
CTFSay_Team_Location(edict_t *who, char *buf)
{
	edict_t *what = NULL;
	edict_t *hot = NULL;
	float hotdist = 999999, newdist;
	vec3_t v;
	int hotindex = 999;
	int i;
	gitem_t *item;
	int nearteam = -1;
	edict_t *flag1, *flag2;
	qboolean hotsee = false;
	qboolean cansee;

	while ((what = loc_findradius(what, who->s.origin, 1024)) != NULL)
	{
		/* find what in loc_classnames */
		for (i = 0; loc_names[i].classname; i++)
		{
			if (strcmp(what->classname, loc_names[i].classname) == 0)
			{
				break;
			}
		}

		if (!loc_names[i].classname)
		{
			continue;
		}

		/* something we can see get priority over something we can't */
		cansee = loc_CanSee(what, who);

		if (cansee && !hotsee)
		{
			hotsee = true;
			hotindex = loc_names[i].priority;
			hot = what;
			VectorSubtract(what->s.origin, who->s.origin, v);
			hotdist = VectorLength(v);
			continue;
		}

		/* if we can't see this, but we have something we can see, skip it */
		if (hotsee && !cansee)
		{
			continue;
		}

		if (hotsee && (hotindex < loc_names[i].priority))
		{
			continue;
		}

		VectorSubtract(what->s.origin, who->s.origin, v);
		newdist = VectorLength(v);

		if ((newdist < hotdist) ||
			(cansee && (loc_names[i].priority < hotindex)))
		{
			hot = what;
			hotdist = newdist;
			hotindex = i;
			hotsee = loc_CanSee(hot, who);
		}
	}

	if (!hot)
	{
		strcpy(buf, "nowhere");
		return;
	}

	/* we now have the closest item
	   see if there's more than one
	   in the map, if so we need to
	   determine what team is closest */
	what = NULL;

	while ((what = G_Find(what, FOFS(classname), hot->classname)) != NULL)
	{
		if (what == hot)
		{
			continue;
		}

		/* if we are here, there is more
		   than one, find out if hot is
		   closer to red flag or blue flag */
		if (((flag1 = G_Find(NULL, FOFS(classname), "item_flag_team1")) != NULL) &&
			((flag2 = G_Find(NULL, FOFS(classname), "item_flag_team2")) != NULL))
		{
			VectorSubtract(hot->s.origin, flag1->s.origin, v);
			hotdist = VectorLength(v);
			VectorSubtract(hot->s.origin, flag2->s.origin, v);
			newdist = VectorLength(v);

			if (hotdist < newdist)
			{
				nearteam = CTF_TEAM1;
			}
			else if (hotdist > newdist)
			{
				nearteam = CTF_TEAM2;
			}
		}

		break;
	}

	if ((item = FindItemByClassname(hot->classname)) == NULL)
	{
		strcpy(buf, "nowhere");
		return;
	}

	/* in water? */
	if (who->waterlevel)
	{
		strcpy(buf, "in the water ");
	}
	else
	{
		*buf = 0;
	}

	/* near or above */
	VectorSubtract(who->s.origin, hot->s.origin, v);

	if ((fabs(v[2]) > fabs(v[0])) && (fabs(v[2]) > fabs(v[1])))
	{
		if (v[2] > 0)
		{
			strcat(buf, "above ");
		}
		else
		{
			strcat(buf, "below ");
		}
	}
	else
	{
		strcat(buf, "near ");
	}

	if (nearteam == CTF_TEAM1)
	{
		strcat(buf, "the red ");
	}
	else if (nearteam == CTF_TEAM2)
	{
		strcat(buf, "the blue ");
	}
	else
	{
		strcat(buf, "the ");
	}

	strcat(buf, item->pickup_name);
}

static void
CTFSay_Team_Armor(edict_t *who, char *buf)
{
	gitem_t *item;
	int index, cells;
	int power_armor_type;

	*buf = 0;

	power_armor_type = PowerArmorType(who);

	if (power_armor_type)
	{
		cells = who->client->pers.inventory[ITEM_INDEX(FindItem("cells"))];

		if (cells)
		{
			sprintf(buf + strlen(buf), "%s with %i cells ",
					(power_armor_type == POWER_ARMOR_SCREEN) ?
					"Power Screen" : "Power Shield", cells);
		}
	}

	index = ArmorIndex(who);

	if (index)
	{
		item = GetItemByIndex(index);

		if (item)
		{
			if (*buf)
			{
				strcat(buf, "and ");
			}

			sprintf(buf + strlen(buf), "%i units of %s",
					who->client->pers.inventory[index], item->pickup_name);
		}
	}

	if (!*buf)
	{
		strcpy(buf, "no armor");
	}
}

static void
CTFSay_Team_Health(edict_t *who, char *buf)
{
	if (who->health <= 0)
	{
		strcpy(buf, "dead");
	}
	else
	{
		sprintf(buf, "%i health", who->health);
	}
}

static void
CTFSay_Team_Tech(edict_t *who, char *buf)
{
	gitem_t *tech;
	int i;

	/* see if the player has a tech powerup */
	i = 0;

	while (tnames[i])
	{
		if (((tech = FindItemByClassname(tnames[i])) != NULL) &&
			who->client->pers.inventory[ITEM_INDEX(tech)])
		{
			sprintf(buf, "the %s", tech->pickup_name);
			return;
		}

		i++;
	}

	strcpy(buf, "no powerup");
}

static void
CTFSay_Team_Weapon(edict_t *who, char *buf)
{
	if (who->client->pers.weapon)
	{
		strcpy(buf, who->client->pers.weapon->pickup_name);
	}
	else
	{
		strcpy(buf, "none");
	}
}

static void
CTFSay_Team_Sight(edict_t *who, char *buf)
{
	int i;
	edict_t *targ;
	int n = 0;
	char s[1024];
	char s2[1024];

	*s = *s2 = 0;

	for (i = 1; i <= maxclients->value; i++)
	{
		targ = g_edicts + i;

		if (!targ->inuse ||
			(targ == who) ||
			!loc_CanSee(targ, who))
		{
			continue;
		}

		if (*s2)
		{
			if (strlen(s) + strlen(s2) + 3 < sizeof(s))
			{
				if (n)
				{
					strcat(s, ", ");
				}

				strcat(s, s2);
				*s2 = 0;
			}

			n++;
		}

		strcpy(s2, targ->client->pers.netname);
	}

	if (*s2)
	{
		if (strlen(s) + strlen(s2) + 6 < sizeof(s))
		{
			if (n)
			{
				strcat(s, " and ");
			}

			strcat(s, s2);
		}

		strcpy(buf, s);
	}
	else
	{
		strcpy(buf, "no one");
	}
}

void
CTFSay_Team(edict_t *who, char *msg)
{
	char outmsg[256];
	char buf[256];
	int i;
	char *p;
	edict_t *cl_ent;

	if (CheckFlood(who))
	{
		return;
	}

	outmsg[0] = 0;

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

	for (p = outmsg; *msg && (p - outmsg) < sizeof(outmsg) - 2; msg++)
	{
		if (*msg == '%')
		{
			switch (*++msg)
			{
				case 'l':
				case 'L':
					CTFSay_Team_Location(who, buf);

					if (strlen(buf) + (p - outmsg) < sizeof(outmsg) - 2)
					{
						strcpy(p, buf);
						p += strlen(buf);
					}

					break;
				case 'a':
				case 'A':
					CTFSay_Team_Armor(who, buf);

					if (strlen(buf) + (p - outmsg) < sizeof(outmsg) - 2)
					{
						strcpy(p, buf);
						p += strlen(buf);
					}

					break;
				case 'h':
				case 'H':
					CTFSay_Team_Health(who, buf);

					if (strlen(buf) + (p - outmsg) < sizeof(outmsg) - 2)
					{
						strcpy(p, buf);
						p += strlen(buf);
					}

					break;
				case 't':
				case 'T':
					CTFSay_Team_Tech(who, buf);

					if (strlen(buf) + (p - outmsg) < sizeof(outmsg) - 2)
					{
						strcpy(p, buf);
						p += strlen(buf);
					}

					break;
				case 'w':
				case 'W':
					CTFSay_Team_Weapon(who, buf);

					if (strlen(buf) + (p - outmsg) < sizeof(outmsg) - 2)
					{
						strcpy(p, buf);
						p += strlen(buf);
					}

					break;

				case 'n':
				case 'N':
					CTFSay_Team_Sight(who, buf);

					if (strlen(buf) + (p - outmsg) < sizeof(outmsg) - 2)
					{
						strcpy(p, buf);
						p += strlen(buf);
					}

					break;

				default:
					*p++ = *msg;
			}
		}
		else
		{
			*p++ = *msg;
		}
	}

	*p = 0;

	for (i = 0; i < maxclients->value; i++)
	{
		cl_ent = g_edicts + 1 + i;

		if (!cl_ent->inuse)
		{
			continue;
		}

		if (cl_ent->client->resp.ctf_team == who->client->resp.ctf_team)
		{
			gi.cprintf(cl_ent, PRINT_CHAT, "(%s): %s\n",
					who->client->pers.netname, outmsg);
		}
	}
}

/*-----------------------------------------------------------------------*/

/*
 * QUAKED misc_ctf_banner (1 .5 0) (-4 -64 0) (4 64 248) TEAM2
 * The origin is the bottom of the banner.
 * The banner is 248 tall.
 */
static void
misc_ctf_banner_think(edict_t *ent)
{
	ent->s.frame = (ent->s.frame + 1) % 16;
	ent->nextthink = level.time + FRAMETIME;
}

void
SP_misc_ctf_banner(edict_t *ent)
{
	ent->movetype = MOVETYPE_NONE;
	ent->solid = SOLID_NOT;
	ent->s.modelindex = gi.modelindex("models/ctf/banner/tris.md2");

	if (ent->spawnflags & 1) /* team2 */
	{
		ent->s.skinnum = 1;
	}

	ent->s.frame = rand() % 16;
	gi.linkentity(ent);

	ent->think = misc_ctf_banner_think;
	ent->nextthink = level.time + FRAMETIME;
}

/*
 * QUAKED misc_ctf_small_banner (1 .5 0) (-4 -32 0) (4 32 124) TEAM2
 * The origin is the bottom of the banner.
 * The banner is 124 tall.
 */
void
SP_misc_ctf_small_banner(edict_t *ent)
{
	ent->movetype = MOVETYPE_NONE;
	ent->solid = SOLID_NOT;
	ent->s.modelindex = gi.modelindex("models/ctf/banner/small.md2");

	if (ent->spawnflags & 1) /* team2 */
	{
		ent->s.skinnum = 1;
	}

	ent->s.frame = rand() % 16;
	gi.linkentity(ent);

	ent->think = misc_ctf_banner_think;
	ent->nextthink = level.time + FRAMETIME;
}

/*-----------------------------------------------------------------------*/

static void
SetLevelName(pmenu_t *p)
{
	static char levelname[33];

	levelname[0] = '*';

	if (g_edicts[0].message)
	{
		Q_strlcpy(levelname + 1, g_edicts[0].message, sizeof(levelname) - 1);
	}
	else
	{
		Q_strlcpy(levelname + 1, level.mapname, sizeof(levelname) - 1);
	}

	levelname[sizeof(levelname) - 1] = 0;
	p->text = levelname;
}

/*-----------------------------------------------------------------------*/

/* ELECTIONS */

static qboolean
CTFBeginElection(edict_t *ent, elect_t type, char *msg)
{
	int i;
	int count;
	edict_t *e;

	if (electpercentage->value == 0)
	{
		gi.cprintf( ent, PRINT_HIGH,
				"Elections are disabled, only an admin can process this action.\n");
		return false;
	}

	if (ctfgame.election != ELECT_NONE)
	{
		gi.cprintf(ent, PRINT_HIGH, "Election already in progress.\n");
		return false;
	}

	/* clear votes */
	count = 0;

	for (i = 1; i <= maxclients->value; i++)
	{
		e = g_edicts + i;
		e->client->resp.voted = false;

		if (e->inuse)
		{
			count++;
		}
	}

	if (count < 2)
	{
		gi.cprintf(ent, PRINT_HIGH, "Not enough players for election.\n");
		return false;
	}

	ctfgame.etarget = ent;
	ctfgame.election = type;
	ctfgame.evotes = 0;
	ctfgame.needvotes = (count * electpercentage->value) / 100;
	ctfgame.electtime = level.time + 20; /* twenty seconds for election */
	Q_strlcpy(ctfgame.emsg, msg, sizeof(ctfgame.emsg));

	/* tell everyone */
	gi.bprintf(PRINT_CHAT, "%s\n", ctfgame.emsg);
	gi.bprintf(PRINT_HIGH, "Type YES or NO to vote on this request.\n");
	gi.bprintf(PRINT_HIGH, "Votes: %d  Needed: %d  Time left: %ds\n",
			ctfgame.evotes, ctfgame.needvotes, (int)(ctfgame.electtime - level.time));

	return true;
}

void DoRespawn(edict_t *ent);

static void
CTFResetAllPlayers(void)
{
	int i;
	edict_t *ent;

	for (i = 1; i <= maxclients->value; i++)
	{
		ent = g_edicts + i;

		if (!ent->inuse)
		{
			continue;
		}

		if (ent->client->menu)
		{
			PMenu_Close(ent);
		}

		CTFPlayerResetGrapple(ent);
		CTFDeadDropFlag(ent);
		CTFDeadDropTech(ent);

		ent->client->resp.ctf_team = CTF_NOTEAM;
		ent->client->resp.ready = false;

		ent->svflags = 0;
		ent->flags &= ~FL_GODMODE;
		PutClientInServer(ent);
	}

	/* reset the level */
	CTFResetTech();
	CTFResetFlags();

	for (ent = g_edicts + 1, i = 1; i < globals.num_edicts; i++, ent++)
	{
		if (ent->inuse && !ent->client)
		{
			if ((ent->solid == SOLID_NOT) && (ent->think == DoRespawn) &&
				(ent->nextthink >= level.time))
			{
				ent->nextthink = 0;
				DoRespawn(ent);
			}
		}
	}

	if (ctfgame.match == MATCH_SETUP)
	{
		ctfgame.matchtime = level.time + matchsetuptime->value * 60;
	}
}

static void
CTFAssignGhost(edict_t *ent)
{
	int ghost, i;

	for (ghost = 0; ghost < MAX_CLIENTS; ghost++)
	{
		if (!ctfgame.ghosts[ghost].code)
		{
			break;
		}
	}

	if (ghost == MAX_CLIENTS)
	{
		return;
	}

	ctfgame.ghosts[ghost].team = ent->client->resp.ctf_team;
	ctfgame.ghosts[ghost].score = 0;

	for ( ; ; )
	{
		ctfgame.ghosts[ghost].code = 10000 + (rand() % 90000);

		for (i = 0; i < MAX_CLIENTS; i++)
		{
			if ((i != ghost) &&
				(ctfgame.ghosts[i].code == ctfgame.ghosts[ghost].code))
			{
				break;
			}
		}

		if (i == MAX_CLIENTS)
		{
			break;
		}
	}

	ctfgame.ghosts[ghost].ent = ent;
	strcpy(ctfgame.ghosts[ghost].netname, ent->client->pers.netname);
	ent->client->resp.ghost = ctfgame.ghosts + ghost;
	gi.cprintf(ent, PRINT_CHAT, "Your ghost code is **** %d ****\n",
			ctfgame.ghosts[ghost].code);
	gi.cprintf(ent, PRINT_HIGH,
			"If you lose connection, you can rejoin with your score "
			"intact by typing \"ghost %d\".\n",
			ctfgame.ghosts[ghost].code);
}

/* start a match */
static void
CTFStartMatch(void)
{
	int i;
	edict_t *ent;

	ctfgame.match = MATCH_GAME;
	ctfgame.matchtime = level.time + matchtime->value * 60;
	ctfgame.countdown = false;

	ctfgame.team1 = ctfgame.team2 = 0;

	memset(ctfgame.ghosts, 0, sizeof(ctfgame.ghosts));

	for (i = 1; i <= maxclients->value; i++)
	{
		ent = g_edicts + i;

		if (!ent->inuse)
		{
			continue;
		}

		ent->client->resp.score = 0;
		ent->client->resp.ctf_state = 0;
		ent->client->resp.ghost = NULL;

		gi.centerprintf(ent,
				"******************\n\nMATCH HAS STARTED!\n\n******************");

		if (ent->client->resp.ctf_team != CTF_NOTEAM)
		{
			/* make up a ghost code */
			CTFAssignGhost(ent);
			CTFPlayerResetGrapple(ent);
			ent->svflags = SVF_NOCLIENT;
			ent->flags &= ~FL_GODMODE;

			ent->client->respawn_time = level.time + 1.0 + ((rand() % 30) / 10.0);
			ent->client->ps.pmove.pm_type = PM_DEAD;
			ent->client->anim_priority = ANIM_DEATH;
			ent->s.frame = FRAME_death308 - 1;
			ent->client->anim_end = FRAME_death308;
			ent->deadflag = DEAD_DEAD;
			ent->movetype = MOVETYPE_NOCLIP;
			ent->client->ps.gunindex = 0;
			gi.linkentity(ent);
		}
	}
}

static void
CTFEndMatch(void)
{
	ctfgame.match = MATCH_POST;
	gi.bprintf(PRINT_CHAT, "MATCH COMPLETED!\n");

	CTFCalcScores();

	gi.bprintf(PRINT_HIGH, "RED TEAM:  %d captures, %d points\n",
			ctfgame.team1, ctfgame.total1);
	gi.bprintf(PRINT_HIGH, "BLUE TEAM:  %d captures, %d points\n",
			ctfgame.team2, ctfgame.total2);

	if (ctfgame.team1 > ctfgame.team2)
	{
		gi.bprintf(PRINT_CHAT, "RED team won over the BLUE team by %d CAPTURES!\n",
				ctfgame.team1 - ctfgame.team2);
	}
	else if (ctfgame.team2 > ctfgame.team1)
	{
		gi.bprintf(PRINT_CHAT, "BLUE team won over the RED team by %d CAPTURES!\n",
				ctfgame.team2 - ctfgame.team1);
	}
	else if (ctfgame.total1 > ctfgame.total2) /* frag tie breaker */
	{
		gi.bprintf(PRINT_CHAT, "RED team won over the BLUE team by %d POINTS!\n",
				ctfgame.total1 - ctfgame.total2);
	}
	else if (ctfgame.total2 > ctfgame.total1)
	{
		gi.bprintf(PRINT_CHAT, "BLUE team won over the RED team by %d POINTS!\n",
				ctfgame.total2 - ctfgame.total1);
	}
	else
	{
		gi.bprintf(PRINT_CHAT, "TIE GAME!\n");
	}

	EndDMLevel();
}

qboolean
CTFNextMap(void)
{
	if (ctfgame.match == MATCH_POST)
	{
		ctfgame.match = MATCH_SETUP;
		CTFResetAllPlayers();
		return true;
	}

	return false;
}

static void
CTFWinElection(void)
{
	switch (ctfgame.election)
	{
		case ELECT_MATCH:

			/* reset into match mode */
			if (competition->value < 3)
			{
				gi.cvar_set("competition", "2");
			}

			ctfgame.match = MATCH_SETUP;
			CTFResetAllPlayers();
			break;

		case ELECT_ADMIN:
			ctfgame.etarget->client->resp.admin = true;
			gi.bprintf(PRINT_HIGH, "%s has become an admin.\n",
				ctfgame.etarget->client->pers.netname);
			gi.cprintf(ctfgame.etarget, PRINT_HIGH,
				"Type 'admin' to access the administration menu.\n");
			break;

		case ELECT_MAP:
			gi.bprintf(PRINT_HIGH, "%s is warping to level %s.\n",
				ctfgame.etarget->client->pers.netname, ctfgame.elevel);
			Q_strlcpy(level.forcemap, ctfgame.elevel, sizeof(level.forcemap));
			EndDMLevel();
			break;
		default:
			break;
	}

	ctfgame.election = ELECT_NONE;
}

void
CTFVoteYes(edict_t *ent)
{
	if (ctfgame.election == ELECT_NONE)
	{
		gi.cprintf(ent, PRINT_HIGH, "No election is in progress.\n");
		return;
	}

	if (ent->client->resp.voted)
	{
		gi.cprintf(ent, PRINT_HIGH, "You already voted.\n");
		return;
	}

	if (ctfgame.etarget == ent)
	{
		gi.cprintf(ent, PRINT_HIGH, "You can't vote for yourself.\n");
		return;
	}

	ent->client->resp.voted = true;

	ctfgame.evotes++;

	if (ctfgame.evotes == ctfgame.needvotes)
	{
		/* the election has been won */
		CTFWinElection();
		return;
	}

	gi.bprintf(PRINT_HIGH, "%s\n", ctfgame.emsg);
	gi.bprintf(PRINT_CHAT, "Votes: %d  Needed: %d  Time left: %ds\n",
			ctfgame.evotes, ctfgame.needvotes, (int)(ctfgame.electtime - level.time));
}

void
CTFVoteNo(edict_t *ent)
{
	if (ctfgame.election == ELECT_NONE)
	{
		gi.cprintf(ent, PRINT_HIGH, "No election is in progress.\n");
		return;
	}

	if (ent->client->resp.voted)
	{
		gi.cprintf(ent, PRINT_HIGH, "You already voted.\n");
		return;
	}

	if (ctfgame.etarget == ent)
	{
		gi.cprintf(ent, PRINT_HIGH, "You can't vote for yourself.\n");
		return;
	}

	ent->client->resp.voted = true;

	gi.bprintf(PRINT_HIGH, "%s\n", ctfgame.emsg);
	gi.bprintf(PRINT_CHAT, "Votes: %d  Needed: %d  Time left: %ds\n",
			ctfgame.evotes, ctfgame.needvotes, (int)(ctfgame.electtime - level.time));
}

void
CTFReady(edict_t *ent)
{
	int i, j;
	edict_t *e;
	int t1, t2;

	if (ent->client->resp.ctf_team == CTF_NOTEAM)
	{
		gi.cprintf(ent, PRINT_HIGH, "Pick a team first (hit <TAB> for menu)\n");
		return;
	}

	if (ctfgame.match != MATCH_SETUP)
	{
		gi.cprintf(ent, PRINT_HIGH, "A match is not being setup.\n");
		return;
	}

	if (ent->client->resp.ready)
	{
		gi.cprintf(ent, PRINT_HIGH, "You have already committed.\n");
		return;
	}

	ent->client->resp.ready = true;
	gi.bprintf(PRINT_HIGH, "%s is ready.\n", ent->client->pers.netname);

	t1 = t2 = 0;

	for (j = 0, i = 1; i <= maxclients->value; i++)
	{
		e = g_edicts + i;

		if (!e->inuse)
		{
			continue;
		}

		if ((e->client->resp.ctf_team != CTF_NOTEAM) && !e->client->resp.ready)
		{
			j++;
		}

		if (e->client->resp.ctf_team == CTF_TEAM1)
		{
			t1++;
		}
		else if (e->client->resp.ctf_team == CTF_TEAM2)
		{
			t2++;
		}
	}

	if (!j && t1 && t2)
	{
		/* everyone has committed */
		gi.bprintf(PRINT_CHAT, "All players have committed.  Match starting\n");
		ctfgame.match = MATCH_PREGAME;
		ctfgame.matchtime = level.time + matchstarttime->value;
		ctfgame.countdown = false;
		gi.positioned_sound(world->s.origin, world, CHAN_AUTO | CHAN_RELIABLE,
				gi.soundindex("misc/talk1.wav"), 1, ATTN_NONE, 0);
	}
}

void
CTFNotReady(edict_t *ent)
{
	if (ent->client->resp.ctf_team == CTF_NOTEAM)
	{
		gi.cprintf(ent, PRINT_HIGH, "Pick a team first (hit <TAB> for menu)\n");
		return;
	}

	if ((ctfgame.match != MATCH_SETUP) && (ctfgame.match != MATCH_PREGAME))
	{
		gi.cprintf(ent, PRINT_HIGH, "A match is not being setup.\n");
		return;
	}

	if (!ent->client->resp.ready)
	{
		gi.cprintf(ent, PRINT_HIGH, "You haven't committed.\n");
		return;
	}

	ent->client->resp.ready = false;
	gi.bprintf(PRINT_HIGH, "%s is no longer ready.\n",
			ent->client->pers.netname);

	if (ctfgame.match == MATCH_PREGAME)
	{
		gi.bprintf(PRINT_CHAT, "Match halted.\n");
		ctfgame.match = MATCH_SETUP;
		ctfgame.matchtime = level.time + matchsetuptime->value * 60;
	}
}

void
CTFGhost(edict_t *ent)
{
	int i;
	int n;

	if (gi.argc() < 2)
	{
		gi.cprintf(ent, PRINT_HIGH, "Usage:  ghost <code>\n");
		return;
	}

	if (ent->client->resp.ctf_team != CTF_NOTEAM)
	{
		gi.cprintf(ent, PRINT_HIGH, "You are already in the game.\n");
		return;
	}

	if (ctfgame.match != MATCH_GAME)
	{
		gi.cprintf(ent, PRINT_HIGH, "No match is in progress.\n");
		return;
	}

	n = atoi(gi.argv(1));

	for (i = 0; i < MAX_CLIENTS; i++)
	{
		if (ctfgame.ghosts[i].code && (ctfgame.ghosts[i].code == n))
		{
			gi.cprintf(ent, PRINT_HIGH,
					"Ghost code accepted, your position has been reinstated.\n");
			ctfgame.ghosts[i].ent->client->resp.ghost = NULL;
			ent->client->resp.ctf_team = ctfgame.ghosts[i].team;
			ent->client->resp.ghost = ctfgame.ghosts + i;
			ent->client->resp.score = ctfgame.ghosts[i].score;
			ent->client->resp.ctf_state = 0;
			ctfgame.ghosts[i].ent = ent;
			ent->svflags = 0;
			ent->flags &= ~FL_GODMODE;
			PutClientInServer(ent);
			gi.bprintf(PRINT_HIGH, "%s has been reinstated to %s team.\n",
					ent->client->pers.netname, CTFTeamName(ent->client->resp.ctf_team));
			return;
		}
	}

	gi.cprintf(ent, PRINT_HIGH, "Invalid ghost code.\n");
}

qboolean
CTFMatchSetup(void)
{
	if ((ctfgame.match == MATCH_SETUP) || (ctfgame.match == MATCH_PREGAME))
	{
		return true;
	}

	return false;
}

qboolean
CTFMatchOn(void)
{
	if (ctfgame.match == MATCH_GAME)
	{
		return true;
	}

	return false;
}

/*-----------------------------------------------------------------------*/

void CTFJoinTeam1(edict_t *ent, pmenuhnd_t *p);
void CTFJoinTeam2(edict_t *ent, pmenuhnd_t *p);
void CTFCredits(edict_t *ent, pmenuhnd_t *p);
void CTFReturnToMain(edict_t *ent, pmenuhnd_t *p);
void CTFChaseCam(edict_t *ent, pmenuhnd_t *p);

pmenu_t creditsmenu[] = {
	{"*Quake II", PMENU_ALIGN_CENTER, NULL},
	{"*ThreeWave Capture the Flag", PMENU_ALIGN_CENTER, NULL},
	{NULL, PMENU_ALIGN_CENTER, NULL},
	{"*Programming", PMENU_ALIGN_CENTER, NULL},
	{"Dave 'Zoid' Kirsch", PMENU_ALIGN_CENTER, NULL},
	{"*Level Design", PMENU_ALIGN_CENTER, NULL},
	{"Christian Antkow", PMENU_ALIGN_CENTER, NULL},
	{"Tim Willits", PMENU_ALIGN_CENTER, NULL},
	{"Dave 'Zoid' Kirsch", PMENU_ALIGN_CENTER, NULL},
	{"*Art", PMENU_ALIGN_CENTER, NULL},
	{"Adrian Carmack Paul Steed", PMENU_ALIGN_CENTER, NULL},
	{"Kevin Cloud", PMENU_ALIGN_CENTER, NULL},
	{"*Sound", PMENU_ALIGN_CENTER, NULL},
	{"Tom 'Bjorn' Klok", PMENU_ALIGN_CENTER, NULL},
	{"*Original CTF Art Design", PMENU_ALIGN_CENTER, NULL},
	{"Brian 'Whaleboy' Cozzens", PMENU_ALIGN_CENTER, NULL},
	{NULL, PMENU_ALIGN_CENTER, NULL},
	{"Return to Main Menu", PMENU_ALIGN_LEFT, CTFReturnToMain}
};

static const int jmenu_level = 2;
static const int jmenu_match = 3;
static const int jmenu_red = 5;
static const int jmenu_blue = 7;
static const int jmenu_chase = 9;
static const int jmenu_reqmatch = 11;

pmenu_t joinmenu[] = {
	{"*Quake II", PMENU_ALIGN_CENTER, NULL},
	{"*ThreeWave Capture the Flag", PMENU_ALIGN_CENTER, NULL},
	{NULL, PMENU_ALIGN_CENTER, NULL},
	{NULL, PMENU_ALIGN_CENTER, NULL},
	{NULL, PMENU_ALIGN_CENTER, NULL},
	{"Join Red Team", PMENU_ALIGN_LEFT, CTFJoinTeam1},
	{NULL, PMENU_ALIGN_LEFT, NULL},
	{"Join Blue Team", PMENU_ALIGN_LEFT, CTFJoinTeam2},
	{NULL, PMENU_ALIGN_LEFT, NULL},
	{"Chase Camera", PMENU_ALIGN_LEFT, CTFChaseCam},
	{"Credits", PMENU_ALIGN_LEFT, CTFCredits},
	{NULL, PMENU_ALIGN_LEFT, NULL},
	{NULL, PMENU_ALIGN_LEFT, NULL},
	{"Use [ and ] to move cursor", PMENU_ALIGN_LEFT, NULL},
	{"ENTER to select", PMENU_ALIGN_LEFT, NULL},
	{"ESC to Exit Menu", PMENU_ALIGN_LEFT, NULL},
	{"(TAB to Return)", PMENU_ALIGN_LEFT, NULL},
	{"v" CTF_STRING_VERSION, PMENU_ALIGN_RIGHT, NULL},
};

pmenu_t nochasemenu[] = {
	{"*Quake II", PMENU_ALIGN_CENTER, NULL},
	{"*ThreeWave Capture the Flag", PMENU_ALIGN_CENTER, NULL},
	{NULL, PMENU_ALIGN_CENTER, NULL},
	{NULL, PMENU_ALIGN_CENTER, NULL},
	{"No one to chase", PMENU_ALIGN_LEFT, NULL},
	{NULL, PMENU_ALIGN_CENTER, NULL},
	{"Return to Main Menu", PMENU_ALIGN_LEFT, CTFReturnToMain}
};

static void
CTFJoinTeam(edict_t *ent, int desired_team)
{
	char *s;

	PMenu_Close(ent);

	ent->svflags &= ~SVF_NOCLIENT;
	ent->client->resp.ctf_team = desired_team;
	ent->client->resp.ctf_state = 0;
	s = Info_ValueForKey(ent->client->pers.userinfo, "skin");
	CTFAssignSkin(ent, s);

	/* assign a ghost if we are in match mode */
	if (ctfgame.match == MATCH_GAME)
	{
		if (ent->client->resp.ghost)
		{
			ent->client->resp.ghost->code = 0;
		}

		ent->client->resp.ghost = NULL;
		CTFAssignGhost(ent);
	}

	PutClientInServer(ent);

	/* add a teleportation effect */
	ent->s.event = EV_PLAYER_TELEPORT;

	/* hold in place briefly */
	ent->client->ps.pmove.pm_flags = PMF_TIME_TELEPORT;
	ent->client->ps.pmove.pm_time = 14;
	gi.bprintf(PRINT_HIGH, "%s joined the %s team.\n",
			ent->client->pers.netname, CTFTeamName(desired_team));

	if (ctfgame.match == MATCH_SETUP)
	{
		gi.centerprintf(ent, "***********************\n"
							 "Type \"ready\" in console\n"
							 "to ready up.\n"
							 "***********************");
	}
}

void
CTFJoinTeam1(edict_t *ent, pmenuhnd_t *p)
{
	CTFJoinTeam(ent, CTF_TEAM1);
}

void
CTFJoinTeam2(edict_t *ent, pmenuhnd_t *p)
{
	CTFJoinTeam(ent, CTF_TEAM2);
}

void
CTFChaseCam(edict_t *ent, pmenuhnd_t *p)
{
	int i;
	edict_t *e;

	if (ent->client->chase_target)
	{
		ent->client->chase_target = NULL;
		ent->client->ps.pmove.pm_flags &= ~PMF_NO_PREDICTION;
		PMenu_Close(ent);
		return;
	}

	for (i = 1; i <= maxclients->value; i++)
	{
		e = g_edicts + i;

		if (e->inuse && (e->solid != SOLID_NOT))
		{
			ent->client->chase_target = e;
			PMenu_Close(ent);
			ent->client->update_chase = true;
			return;
		}
	}

	SetLevelName(nochasemenu + jmenu_level);

	PMenu_Close(ent);
	PMenu_Open(ent, nochasemenu, -1,
			sizeof(nochasemenu) / sizeof(pmenu_t),
			NULL);
}

void
CTFReturnToMain(edict_t *ent, pmenuhnd_t *p)
{
	PMenu_Close(ent);
	CTFOpenJoinMenu(ent);
}

static void
CTFRequestMatch(edict_t *ent, pmenuhnd_t *p)
{
	char text[1024];

	PMenu_Close(ent);

	sprintf(text, "%s has requested to switch to competition mode.",
			ent->client->pers.netname);
	CTFBeginElection(ent, ELECT_MATCH, text);
}

static int
CTFUpdateJoinMenu(edict_t *ent)
{
	static char team1players[32];
	static char team2players[32];
	int num1, num2, i;

	if ((ctfgame.match >= MATCH_PREGAME) && matchlock->value)
	{
		joinmenu[jmenu_red].text = "MATCH IS LOCKED";
		joinmenu[jmenu_red].SelectFunc = NULL;
		joinmenu[jmenu_blue].text = "  (entry is not permitted)";
		joinmenu[jmenu_blue].SelectFunc = NULL;
	}
	else
	{
		if (ctfgame.match >= MATCH_PREGAME)
		{
			joinmenu[jmenu_red].text = "Join Red MATCH Team";
			joinmenu[jmenu_blue].text = "Join Blue MATCH Team";
		}
		else
		{
			joinmenu[jmenu_red].text = "Join Red Team";
			joinmenu[jmenu_blue].text = "Join Blue Team";
		}

		joinmenu[jmenu_red].SelectFunc = CTFJoinTeam1;
		joinmenu[jmenu_blue].SelectFunc = CTFJoinTeam2;
	}

	if (ctf_forcejoin->string && *ctf_forcejoin->string)
	{
		if (Q_stricmp(ctf_forcejoin->string, "red") == 0)
		{
			joinmenu[jmenu_blue].text = NULL;
			joinmenu[jmenu_blue].SelectFunc = NULL;
		}
		else if (Q_stricmp(ctf_forcejoin->string, "blue") == 0)
		{
			joinmenu[jmenu_red].text = NULL;
			joinmenu[jmenu_red].SelectFunc = NULL;
		}
	}

	if (ent->client->chase_target)
	{
		joinmenu[jmenu_chase].text = "Leave Chase Camera";
	}
	else
	{
		joinmenu[jmenu_chase].text = "Chase Camera";
	}

	SetLevelName(joinmenu + jmenu_level);

	num1 = num2 = 0;

	for (i = 0; i < maxclients->value; i++)
	{
		if (!g_edicts[i + 1].inuse)
		{
			continue;
		}

		if (game.clients[i].resp.ctf_team == CTF_TEAM1)
		{
			num1++;
		}
		else if (game.clients[i].resp.ctf_team == CTF_TEAM2)
		{
			num2++;
		}
	}

	sprintf(team1players, "  (%d players)", num1);
	sprintf(team2players, "  (%d players)", num2);

	switch (ctfgame.match)
	{
		case MATCH_NONE:
			joinmenu[jmenu_match].text = NULL;
			break;

		case MATCH_SETUP:
			joinmenu[jmenu_match].text = "*MATCH SETUP IN PROGRESS";
			break;

		case MATCH_PREGAME:
			joinmenu[jmenu_match].text = "*MATCH STARTING";
			break;

		case MATCH_GAME:
			joinmenu[jmenu_match].text = "*MATCH IN PROGRESS";
			break;
		default:
			break;
	}

	if (joinmenu[jmenu_red].text)
	{
		joinmenu[jmenu_red + 1].text = team1players;
	}
	else
	{
		joinmenu[jmenu_red + 1].text = NULL;
	}

	if (joinmenu[jmenu_blue].text)
	{
		joinmenu[jmenu_blue + 1].text = team2players;
	}
	else
	{
		joinmenu[jmenu_blue + 1].text = NULL;
	}

	joinmenu[jmenu_reqmatch].text = NULL;
	joinmenu[jmenu_reqmatch].SelectFunc = NULL;

	if (competition->value && (ctfgame.match < MATCH_SETUP))
	{
		joinmenu[jmenu_reqmatch].text = "Request Match";
		joinmenu[jmenu_reqmatch].SelectFunc = CTFRequestMatch;
	}

	if (num1 > num2)
	{
		return CTF_TEAM1;
	}
	else if (num2 > num1)
	{
		return CTF_TEAM2;
	}

	return (rand() & 1) ? CTF_TEAM1 : CTF_TEAM2;
}

void
CTFOpenJoinMenu(edict_t *ent)
{
	int team;

	team = CTFUpdateJoinMenu(ent);

	if (ent->client->chase_target)
	{
		team = 8;
	}
	else if (team == CTF_TEAM1)
	{
		team = 4;
	}
	else
	{
		team = 6;
	}

	PMenu_Open(ent, joinmenu, team, sizeof(joinmenu) / sizeof(pmenu_t), NULL);
}

void
CTFCredits(edict_t *ent, pmenuhnd_t *p)
{
	PMenu_Close(ent);
	PMenu_Open(ent, creditsmenu, -1,
			sizeof(creditsmenu) / sizeof(pmenu_t),
			NULL);
}

qboolean
CTFStartClient(edict_t *ent)
{
	if (!ctf->value)
	{
		return false;
	}

	if (ent->client->resp.ctf_team != CTF_NOTEAM)
	{
		return false;
	}

	if (!((int)dmflags->value & DF_CTF_FORCEJOIN) ||
		(ctfgame.match >= MATCH_SETUP))
	{
		/* start as 'observer' */
		ent->movetype = MOVETYPE_NOCLIP;
		ent->solid = SOLID_NOT;
		ent->svflags |= SVF_NOCLIENT;
		ent->client->resp.ctf_team = CTF_NOTEAM;
		ent->client->ps.gunindex = 0;
		gi.linkentity(ent);

		CTFOpenJoinMenu(ent);
		return true;
	}

	return false;
}

void
CTFObserver(edict_t *ent)
{
	char userinfo[MAX_INFO_STRING];

	/* start as 'observer' */
	if (ent->movetype == MOVETYPE_NOCLIP)
	{
		CTFPlayerResetGrapple(ent);
	}

	CTFDeadDropFlag(ent);
	CTFDeadDropTech(ent);

	ent->deadflag = DEAD_NO;
	ent->movetype = MOVETYPE_NOCLIP;
	ent->solid = SOLID_NOT;
	ent->svflags |= SVF_NOCLIENT;
	ent->client->resp.ctf_team = CTF_NOTEAM;
	ent->client->ps.gunindex = 0;
	ent->client->resp.score = 0;
	memcpy(userinfo, ent->client->pers.userinfo, sizeof(userinfo));
	InitClientPersistant(ent);
	ClientUserinfoChanged(ent, userinfo);
	gi.linkentity(ent);
	CTFOpenJoinMenu(ent);
}

qboolean
CTFInMatch(void)
{
	if (ctfgame.match > MATCH_NONE)
	{
		return true;
	}

	return false;
}

qboolean
CTFCheckRules(void)
{
	int t;
	int i, j;
	char text[64];
	edict_t *ent;

	if ((ctfgame.election != ELECT_NONE) && (ctfgame.electtime <= level.time))
	{
		gi.bprintf(PRINT_CHAT, "Election timed out and has been cancelled.\n");
		ctfgame.election = ELECT_NONE;
	}

	if (ctfgame.match != MATCH_NONE)
	{
		t = ctfgame.matchtime - level.time;

		/* no team warnings in match mode */
		ctfgame.warnactive = 0;

		if (t <= 0) /* time ended on something */
		{
			switch (ctfgame.match)
			{
				case MATCH_SETUP:

					/* go back to normal mode */
					if (competition->value < 3)
					{
						ctfgame.match = MATCH_NONE;
						gi.cvar_set("competition", "1");
						CTFResetAllPlayers();
					}
					else
					{
						/* reset the time */
						ctfgame.matchtime = level.time + matchsetuptime->value * 60;
					}

					return false;

				case MATCH_PREGAME:
					/* match started! */
					CTFStartMatch();
					gi.positioned_sound(world->s.origin, world,
						CHAN_AUTO | CHAN_RELIABLE,
						gi.soundindex("misc/tele_up.wav"),
						1, ATTN_NONE, 0);
					return false;

				case MATCH_GAME:
					/* match ended! */
					CTFEndMatch();
					gi.positioned_sound(world->s.origin,
						world, CHAN_AUTO | CHAN_RELIABLE,
						gi.soundindex("misc/bigtele.wav"),
						1, ATTN_NONE, 0);
					return false;
				default:
					break;
			}
		}

		if (t == ctfgame.lasttime)
		{
			return false;
		}

		ctfgame.lasttime = t;

		switch (ctfgame.match)
		{
			case MATCH_SETUP:

				for (j = 0, i = 1; i <= maxclients->value; i++)
				{
					ent = g_edicts + i;

					if (!ent->inuse)
					{
						continue;
					}

					if ((ent->client->resp.ctf_team != CTF_NOTEAM) &&
						!ent->client->resp.ready)
					{
						j++;
					}
				}

				if (competition->value < 3)
				{
					sprintf(text, "%02d:%02d SETUP: %d not ready",
							t / 60, t % 60, j);
				}
				else
				{
					sprintf(text, "SETUP: %d not ready", j);
				}

				gi.configstring(CONFIG_CTF_MATCH, text);
				break;

			case MATCH_PREGAME:
				sprintf(text, "%02d:%02d UNTIL START", t / 60, t % 60);
				gi.configstring(CONFIG_CTF_MATCH, text);

				if ((t <= 10) && !ctfgame.countdown)
				{
					ctfgame.countdown = true;
					gi.positioned_sound(world->s.origin, world,
							CHAN_AUTO | CHAN_RELIABLE,
							gi.soundindex("world/10_0.wav"),
							1, ATTN_NONE, 0);
				}

				break;

			case MATCH_GAME:
				sprintf(text, "%02d:%02d MATCH", t / 60, t % 60);
				gi.configstring(CONFIG_CTF_MATCH, text);

				if ((t <= 10) && !ctfgame.countdown)
				{
					ctfgame.countdown = true;
					gi.positioned_sound(world->s.origin, world,
							CHAN_AUTO | CHAN_RELIABLE,
							gi.soundindex("world/10_0.wav"),
							1, ATTN_NONE, 0);
				}

				break;
			default:
				break;
		}

		return false;
	}
	else
	{
		int team1 = 0, team2 = 0;

		if (level.time == ctfgame.lasttime)
		{
			return false;
		}

		ctfgame.lasttime = level.time;

		/* this is only done in non-match (public) mode */
		if (warn_unbalanced->value)
		{
			/* count up the team totals */
			for (i = 1; i <= maxclients->value; i++)
			{
				ent = g_edicts + i;

				if (!ent->inuse)
				{
					continue;
				}

				if (ent->client->resp.ctf_team == CTF_TEAM1)
				{
					team1++;
				}
				else if (ent->client->resp.ctf_team == CTF_TEAM2)
				{
					team2++;
				}
			}

			if ((team1 - team2 >= 2) && (team2 >= 2))
			{
				if (ctfgame.warnactive != CTF_TEAM1)
				{
					ctfgame.warnactive = CTF_TEAM1;
					gi.configstring(CONFIG_CTF_TEAMINFO, "WARNING: Red has too many players");
				}
			}
			else if ((team2 - team1 >= 2) && (team1 >= 2))
			{
				if (ctfgame.warnactive != CTF_TEAM2)
				{
					ctfgame.warnactive = CTF_TEAM2;
					gi.configstring(CONFIG_CTF_TEAMINFO, "WARNING: Blue has too many players");
				}
			}
			else
			{
				ctfgame.warnactive = 0;
			}
		}
		else
		{
			ctfgame.warnactive = 0;
		}
	}

	if (capturelimit->value &&
		((ctfgame.team1 >= capturelimit->value) ||
		 (ctfgame.team2 >= capturelimit->value)))
	{
		gi.bprintf(PRINT_HIGH, "Capturelimit hit.\n");
		return true;
	}

	return false;
}

/*----------------------------------------------------------------------------------*/
/* ADMIN */

typedef struct admin_settings_s
{
	int matchlen;
	int matchsetuplen;
	int matchstartlen;
	qboolean weaponsstay;
	qboolean instantitems;
	qboolean quaddrop;
	qboolean instantweap;
	qboolean matchlock;
} admin_settings_t;

#define SETMENU_SIZE (7 + 5)

static void CTFAdmin_UpdateSettings(edict_t *ent, pmenuhnd_t *setmenu);
void CTFOpenAdminMenu(edict_t *ent);

static void
CTFAdmin_SettingsApply(edict_t *ent, pmenuhnd_t *p)
{
	admin_settings_t *settings = p->arg;
	char st[80];
	int i;

	if (settings->matchlen != matchtime->value)
	{
		gi.bprintf(PRINT_HIGH, "%s changed the match length to %d minutes.\n",
				ent->client->pers.netname, settings->matchlen);

		if (ctfgame.match == MATCH_GAME)
		{
			/* in the middle of a match, change it on the fly */
			ctfgame.matchtime = (ctfgame.matchtime - matchtime->value *
				 60) + settings->matchlen * 60;
		}

		sprintf(st, "%d", settings->matchlen);
		gi.cvar_set("matchtime", st);
	}

	if (settings->matchsetuplen != matchsetuptime->value)
	{
		gi.bprintf(PRINT_HIGH, "%s changed the match setup time to %d minutes.\n",
				ent->client->pers.netname, settings->matchsetuplen);

		if (ctfgame.match == MATCH_SETUP)
		{
			/* in the middle of a match, change it on the fly */
			ctfgame.matchtime =
				(ctfgame.matchtime - matchsetuptime->value *
				 60) + settings->matchsetuplen * 60;
		}

		sprintf(st, "%d", settings->matchsetuplen);
		gi.cvar_set("matchsetuptime", st);
	}

	if (settings->matchstartlen != matchstarttime->value)
	{
		gi.bprintf(PRINT_HIGH, "%s changed the match start time to %d seconds.\n",
				ent->client->pers.netname, settings->matchstartlen);

		if (ctfgame.match == MATCH_PREGAME)
		{
			/* in the middle of a match, change it on the fly */
			ctfgame.matchtime =
				(ctfgame.matchtime -
				 matchstarttime->value) + settings->matchstartlen;
		}

		sprintf(st, "%d", settings->matchstartlen);
		gi.cvar_set("matchstarttime", st);
	}

	if (settings->weaponsstay != !!((int)dmflags->value & DF_WEAPONS_STAY))
	{
		gi.bprintf(PRINT_HIGH, "%s turned %s weapons stay.\n",
				ent->client->pers.netname, settings->weaponsstay ? "on" : "off");
		i = (int)dmflags->value;

		if (settings->weaponsstay)
		{
			i |= DF_WEAPONS_STAY;
		}
		else
		{
			i &= ~DF_WEAPONS_STAY;
		}

		sprintf(st, "%d", i);
		gi.cvar_set("dmflags", st);
	}

	if (settings->instantitems != !!((int)dmflags->value & DF_INSTANT_ITEMS))
	{
		gi.bprintf(PRINT_HIGH, "%s turned %s instant items.\n",
				ent->client->pers.netname, settings->instantitems ? "on" : "off");
		i = (int)dmflags->value;

		if (settings->instantitems)
		{
			i |= DF_INSTANT_ITEMS;
		}
		else
		{
			i &= ~DF_INSTANT_ITEMS;
		}

		sprintf(st, "%d", i);
		gi.cvar_set("dmflags", st);
	}

	if (settings->quaddrop != !!((int)dmflags->value & DF_QUAD_DROP))
	{
		gi.bprintf(PRINT_HIGH, "%s turned %s quad drop.\n",
				ent->client->pers.netname, settings->quaddrop ? "on" : "off");
		i = (int)dmflags->value;

		if (settings->quaddrop)
		{
			i |= DF_QUAD_DROP;
		}
		else
		{
			i &= ~DF_QUAD_DROP;
		}

		sprintf(st, "%d", i);
		gi.cvar_set("dmflags", st);
	}

	if (settings->instantweap != !!((int)instantweap->value))
	{
		gi.bprintf(PRINT_HIGH, "%s turned %s instant weapons.\n",
				ent->client->pers.netname, settings->instantweap ? "on" : "off");
		sprintf(st, "%d", (int)settings->instantweap);
		gi.cvar_set("instantweap", st);
	}

	if (settings->matchlock != !!((int)matchlock->value))
	{
		gi.bprintf(PRINT_HIGH, "%s turned %s match lock.\n",
				ent->client->pers.netname, settings->matchlock ? "on" : "off");
		sprintf(st, "%d", (int)settings->matchlock);
		gi.cvar_set("matchlock", st);
	}

	PMenu_Close(ent);
	CTFOpenAdminMenu(ent);
}

static void
CTFAdmin_SettingsCancel(edict_t *ent, pmenuhnd_t *p)
{
	PMenu_Close(ent);
	CTFOpenAdminMenu(ent);
}

static void
CTFAdmin_ChangeMatchLen(edict_t *ent, pmenuhnd_t *p)
{
	admin_settings_t *settings = p->arg;

	settings->matchlen = (settings->matchlen % 60) + 5;

	if (settings->matchlen < 5)
	{
		settings->matchlen = 5;
	}

	CTFAdmin_UpdateSettings(ent, p);
}

static void
CTFAdmin_ChangeMatchSetupLen(edict_t *ent, pmenuhnd_t *p)
{
	admin_settings_t *settings = p->arg;

	settings->matchsetuplen = (settings->matchsetuplen % 60) + 5;

	if (settings->matchsetuplen < 5)
	{
		settings->matchsetuplen = 5;
	}

	CTFAdmin_UpdateSettings(ent, p);
}

static void
CTFAdmin_ChangeMatchStartLen(edict_t *ent, pmenuhnd_t *p)
{
	admin_settings_t *settings = p->arg;

	settings->matchstartlen = (settings->matchstartlen % 600) + 10;

	if (settings->matchstartlen < 20)
	{
		settings->matchstartlen = 20;
	}

	CTFAdmin_UpdateSettings(ent, p);
}

static void
CTFAdmin_ChangeWeapStay(edict_t *ent, pmenuhnd_t *p)
{
	admin_settings_t *settings = p->arg;

	settings->weaponsstay = !settings->weaponsstay;
	CTFAdmin_UpdateSettings(ent, p);
}

static void
CTFAdmin_ChangeInstantItems(edict_t *ent, pmenuhnd_t *p)
{
	admin_settings_t *settings = p->arg;

	settings->instantitems = !settings->instantitems;
	CTFAdmin_UpdateSettings(ent, p);
}

static void
CTFAdmin_ChangeQuadDrop(edict_t *ent, pmenuhnd_t *p)
{
	admin_settings_t *settings = p->arg;

	settings->quaddrop = !settings->quaddrop;
	CTFAdmin_UpdateSettings(ent, p);
}

static void
CTFAdmin_ChangeInstantWeap(edict_t *ent, pmenuhnd_t *p)
{
	admin_settings_t *settings = p->arg;

	settings->instantweap = !settings->instantweap;
	CTFAdmin_UpdateSettings(ent, p);
}

static void
CTFAdmin_ChangeMatchLock(edict_t *ent, pmenuhnd_t *p)
{
	admin_settings_t *settings = p->arg;

	settings->matchlock = !settings->matchlock;
	CTFAdmin_UpdateSettings(ent, p);
}

static void
CTFAdmin_UpdateSettings(edict_t *ent, pmenuhnd_t *setmenu)
{
	int i = 2;
	char text[64];
	admin_settings_t *settings = setmenu->arg;

	sprintf(text, "Match Len:       %2d mins", settings->matchlen);
	PMenu_UpdateEntry(setmenu->entries + i, text,
			PMENU_ALIGN_LEFT, CTFAdmin_ChangeMatchLen);
	i++;

	sprintf(text, "Match Setup Len: %2d mins", settings->matchsetuplen);
	PMenu_UpdateEntry(setmenu->entries + i, text,
			PMENU_ALIGN_LEFT, CTFAdmin_ChangeMatchSetupLen);
	i++;

	sprintf(text, "Match Start Len: %2d secs", settings->matchstartlen);
	PMenu_UpdateEntry(setmenu->entries + i, text,
			PMENU_ALIGN_LEFT, CTFAdmin_ChangeMatchStartLen);
	i++;

	sprintf(text, "Weapons Stay:    %s", settings->weaponsstay ? "Yes" : "No");
	PMenu_UpdateEntry(setmenu->entries + i, text,
			PMENU_ALIGN_LEFT, CTFAdmin_ChangeWeapStay);
	i++;

	sprintf(text, "Instant Items:   %s", settings->instantitems ? "Yes" : "No");
	PMenu_UpdateEntry(setmenu->entries + i, text,
			PMENU_ALIGN_LEFT, CTFAdmin_ChangeInstantItems);
	i++;

	sprintf(text, "Quad Drop:       %s", settings->quaddrop ? "Yes" : "No");
	PMenu_UpdateEntry(setmenu->entries + i, text,
			PMENU_ALIGN_LEFT, CTFAdmin_ChangeQuadDrop);
	i++;

	sprintf(text, "Instant Weapons: %s", settings->instantweap ? "Yes" : "No");
	PMenu_UpdateEntry(setmenu->entries + i, text,
			PMENU_ALIGN_LEFT, CTFAdmin_ChangeInstantWeap);
	i++;

	sprintf(text, "Match Lock:      %s", settings->matchlock ? "Yes" : "No");
	PMenu_UpdateEntry(setmenu->entries + i, text,
			PMENU_ALIGN_LEFT, CTFAdmin_ChangeMatchLock);
	i++;

	PMenu_Update(ent);
}

pmenu_t def_setmenu[] = {
	{"*Settings Menu", PMENU_ALIGN_CENTER, NULL},
	{NULL, PMENU_ALIGN_CENTER, NULL},
	{NULL, PMENU_ALIGN_LEFT, NULL},                 /* int matchlen; */
	{NULL, PMENU_ALIGN_LEFT, NULL},                 /* int matchsetuplen; */
	{NULL, PMENU_ALIGN_LEFT, NULL},                 /* int matchstartlen; */
	{NULL, PMENU_ALIGN_LEFT, NULL},                 /* qboolean weaponsstay; */
	{NULL, PMENU_ALIGN_LEFT, NULL},                 /* qboolean instantitems; */
	{NULL, PMENU_ALIGN_LEFT, NULL},                 /* qboolean quaddrop; */
	{NULL, PMENU_ALIGN_LEFT, NULL},                 /* qboolean instantweap; */
	{NULL, PMENU_ALIGN_LEFT, NULL},                 /* qboolean matchlock; */
	{NULL, PMENU_ALIGN_LEFT, NULL},
	{"Apply", PMENU_ALIGN_LEFT, CTFAdmin_SettingsApply},
	{"Cancel", PMENU_ALIGN_LEFT, CTFAdmin_SettingsCancel}
};

static void
CTFAdmin_Settings(edict_t *ent, pmenuhnd_t *p)
{
	admin_settings_t *settings;
	pmenuhnd_t *menu;

	PMenu_Close(ent);

	settings = malloc(sizeof(*settings));

	settings->matchlen = matchtime->value;
	settings->matchsetuplen = matchsetuptime->value;
	settings->matchstartlen = matchstarttime->value;
	settings->weaponsstay = !!((int)dmflags->value & DF_WEAPONS_STAY);
	settings->instantitems = !!((int)dmflags->value & DF_INSTANT_ITEMS);
	settings->quaddrop = !!((int)dmflags->value & DF_QUAD_DROP);
	settings->instantweap = instantweap->value != 0;
	settings->matchlock = matchlock->value != 0;

	menu =
		PMenu_Open(ent, def_setmenu, -1,
				sizeof(def_setmenu) / sizeof(pmenu_t),
				settings);
	CTFAdmin_UpdateSettings(ent, menu);
}

static void
CTFAdmin_MatchSet(edict_t *ent, pmenuhnd_t *p)
{
	PMenu_Close(ent);

	if (ctfgame.match == MATCH_SETUP)
	{
		gi.bprintf(PRINT_CHAT, "Match has been forced to start.\n");
		ctfgame.match = MATCH_PREGAME;
		ctfgame.matchtime = level.time + matchstarttime->value;
		gi.positioned_sound(world->s.origin, world,
				CHAN_AUTO | CHAN_RELIABLE,
				gi.soundindex("misc/talk1.wav"),
				1, ATTN_NONE, 0);
		ctfgame.countdown = false;
	}
	else if (ctfgame.match == MATCH_GAME)
	{
		gi.bprintf(PRINT_CHAT, "Match has been forced to terminate.\n");
		ctfgame.match = MATCH_SETUP;
		ctfgame.matchtime = level.time + matchsetuptime->value * 60;
		CTFResetAllPlayers();
	}
}

static void
CTFAdmin_MatchMode(edict_t *ent, pmenuhnd_t *p)
{
	PMenu_Close(ent);

	if (ctfgame.match != MATCH_SETUP)
	{
		if (competition->value < 3)
		{
			gi.cvar_set("competition", "2");
		}

		ctfgame.match = MATCH_SETUP;
		CTFResetAllPlayers();
	}
}

static void
CTFAdmin_Reset(edict_t *ent, pmenuhnd_t *p)
{
	PMenu_Close(ent);

	/* go back to normal mode */
	gi.bprintf(PRINT_CHAT, "Match mode has been terminated, resetting to normal game.\n");
	ctfgame.match = MATCH_NONE;
	gi.cvar_set("competition", "1");
	CTFResetAllPlayers();
}

static void
CTFAdmin_Cancel(edict_t *ent, pmenuhnd_t *p)
{
	PMenu_Close(ent);
}

pmenu_t adminmenu[] = {
	{"*Administration Menu", PMENU_ALIGN_CENTER, NULL},
	{NULL, PMENU_ALIGN_CENTER, NULL}, /* blank */
	{"Settings", PMENU_ALIGN_LEFT, CTFAdmin_Settings},
	{NULL, PMENU_ALIGN_LEFT, NULL},
	{NULL, PMENU_ALIGN_LEFT, NULL},
	{"Cancel", PMENU_ALIGN_LEFT, CTFAdmin_Cancel},
	{NULL, PMENU_ALIGN_CENTER, NULL},
};

void
CTFOpenAdminMenu(edict_t *ent)
{
	adminmenu[3].text = NULL;
	adminmenu[3].SelectFunc = NULL;
	adminmenu[4].text = NULL;
	adminmenu[4].SelectFunc = NULL;

	if (ctfgame.match == MATCH_SETUP)
	{
		adminmenu[3].text = "Force start match";
		adminmenu[3].SelectFunc = CTFAdmin_MatchSet;
		adminmenu[4].text = "Reset to pickup mode";
		adminmenu[4].SelectFunc = CTFAdmin_Reset;
	}
	else if ((ctfgame.match == MATCH_GAME) || (ctfgame.match == MATCH_PREGAME))
	{
		adminmenu[3].text = "Cancel match";
		adminmenu[3].SelectFunc = CTFAdmin_MatchSet;
	}
	else if ((ctfgame.match == MATCH_NONE) && competition->value)
	{
		adminmenu[3].text = "Switch to match mode";
		adminmenu[3].SelectFunc = CTFAdmin_MatchMode;
	}

	PMenu_Open(ent, adminmenu, -1, sizeof(adminmenu) / sizeof(pmenu_t), NULL);
}

void
CTFAdmin(edict_t *ent)
{
	char text[1024];

	if (!allow_admin->value)
	{
		gi.cprintf(ent, PRINT_HIGH, "Administration is disabled\n");
		return;
	}

	if ((gi.argc() > 1) && admin_password->string && *admin_password->string &&
		!ent->client->resp.admin &&
		(strcmp(admin_password->string, gi.argv(1)) == 0))
	{
		ent->client->resp.admin = true;
		gi.bprintf(PRINT_HIGH, "%s has become an admin.\n",
				ent->client->pers.netname);
		gi.cprintf(ent, PRINT_HIGH, "Type 'admin' to access the administration menu.\n");
	}

	if (!ent->client->resp.admin)
	{
		sprintf(text, "%s has requested admin rights.",
				ent->client->pers.netname);
		CTFBeginElection(ent, ELECT_ADMIN, text);
		return;
	}

	if (ent->client->menu)
	{
		PMenu_Close(ent);
	}

	CTFOpenAdminMenu(ent);
}

/*----------------------------------------------------------------*/

void
CTFStats(edict_t *ent)
{
	int i, e;
	ghost_t *g;
	char st[80];
	char text[1024];
	edict_t *e2;

	*text = 0;

	if (ctfgame.match == MATCH_SETUP)
	{
		for (i = 1; i <= maxclients->value; i++)
		{
			e2 = g_edicts + i;

			if (!e2->inuse)
			{
				continue;
			}

			if (!e2->client->resp.ready &&
				(e2->client->resp.ctf_team != CTF_NOTEAM))
			{
				sprintf(st, "%s is not ready.\n", e2->client->pers.netname);

				if (strlen(text) + strlen(st) < sizeof(text) - 50)
				{
					strcat(text, st);
				}
			}
		}
	}

	for (i = 0, g = ctfgame.ghosts; i < MAX_CLIENTS; i++, g++)
	{
		if (g->ent)
		{
			break;
		}
	}

	if (i == MAX_CLIENTS)
	{
		if (*text)
		{
			gi.cprintf(ent, PRINT_HIGH, "%s", text);
		}

		gi.cprintf(ent, PRINT_HIGH, "No statistics available.\n");
		return;
	}

	strcat(text, "  #|Name            |Score|Kills|Death|BasDf|CarDf|Effcy|\n");

	for (i = 0, g = ctfgame.ghosts; i < MAX_CLIENTS; i++, g++)
	{
		if (!*g->netname)
		{
			continue;
		}

		if (g->deaths + g->kills == 0)
		{
			e = 50;
		}
		else
		{
			e = g->kills * 100 / (g->kills + g->deaths);
		}

		sprintf(st, "%3d|%-16.16s|%5d|%5d|%5d|%5d|%5d|%4d%%|\n",
				g->number, g->netname, g->score, g->kills,
				g->deaths, g->basedef, g->carrierdef, e);

		if (strlen(text) + strlen(st) > sizeof(text) - 50)
		{
			sprintf(text + strlen(text), "And more...\n");
			gi.cprintf(ent, PRINT_HIGH, "%s", text);
			return;
		}

		strcat(text, st);
	}

	gi.cprintf(ent, PRINT_HIGH, "%s", text);
}

void
CTFPlayerList(edict_t *ent)
{
	int i;
	char st[80];
	char text[1400];
	edict_t *e2;

	/* number, name, connect time, ping, score, admin */
	*text = 0;

	for (i = 1; i <= maxclients->value; i++)
	{
		e2 = g_edicts + i;

		if (!e2->inuse)
		{
			continue;
		}

		Com_sprintf( st, sizeof(st),
				"%3d %-16.16s %02d:%02d %4d %3d%s%s\n",
				i, e2->client->pers.netname,
				(level.framenum - e2->client->resp.enterframe) / 600,
				((level.framenum - e2->client->resp.enterframe) % 600) / 10,
				e2->client->ping, e2->client->resp.score,
				(ctfgame.match == MATCH_SETUP || ctfgame.match ==
				 MATCH_PREGAME) ?  (e2->client->resp.ready ? " (ready)" : " (notready)") : "",
				e2->client->resp.admin ? " (admin)" : "");

		if (strlen(text) + strlen(st) > sizeof(text) - 50)
		{
			sprintf(text + strlen(text), "And more...\n");
			gi.cprintf(ent, PRINT_HIGH, "%s", text);
			return;
		}

		strcat(text, st);
	}

	gi.cprintf(ent, PRINT_HIGH, "%s", text);
}

void
CTFWarp(edict_t *ent)
{
	char text[1024];
	char *mlist, *token;
	static const char *seps = " \t\n\r";

	if (gi.argc() < 2)
	{
		gi.cprintf(ent, PRINT_HIGH, "Where do you want to warp to?\n");
		gi.cprintf(ent, PRINT_HIGH, "Available levels are: %s\n",
				warp_list->string);
		return;
	}

	mlist = strdup(warp_list->string);

	token = strtok(mlist, seps);

	while (token != NULL)
	{
		if (Q_stricmp(token, gi.argv(1)) == 0)
		{
			break;
		}

		token = strtok(NULL, seps);
	}

	if (token == NULL)
	{
		gi.cprintf(ent, PRINT_HIGH, "Unknown CTF level.\n");
		gi.cprintf(ent, PRINT_HIGH, "Available levels are: %s\n",
				warp_list->string);
		free(mlist);
		return;
	}

	free(mlist);

	if (ent->client->resp.admin)
	{
		gi.bprintf(PRINT_HIGH, "%s is warping to level %s.\n",
				ent->client->pers.netname, gi.argv(1));
		Q_strlcpy(level.forcemap, gi.argv(1), sizeof(level.forcemap));
		EndDMLevel();
		return;
	}

	sprintf(text, "%s has requested warping to level %s.",
			ent->client->pers.netname, gi.argv(1));

	if (CTFBeginElection(ent, ELECT_MAP, text))
	{
		Q_strlcpy(ctfgame.elevel, gi.argv(1), sizeof(ctfgame.elevel));
	}
}

void
CTFBoot(edict_t *ent)
{
	int i;
	edict_t *targ;
	char text[80];

	if (!ent->client->resp.admin)
	{
		gi.cprintf(ent, PRINT_HIGH, "You are not an admin.\n");
		return;
	}

	if (gi.argc() < 2)
	{
		gi.cprintf(ent, PRINT_HIGH, "Who do you want to kick?\n");
		return;
	}

	if ((*gi.argv(1) < '0') && (*gi.argv(1) > '9'))
	{
		gi.cprintf(ent, PRINT_HIGH, "Specify the player number to kick.\n");
		return;
	}

	i = atoi(gi.argv(1));

	if ((i < 1) || (i > maxclients->value))
	{
		gi.cprintf(ent, PRINT_HIGH, "Invalid player number.\n");
		return;
	}

	targ = g_edicts + i;

	if (!targ->inuse)
	{
		gi.cprintf(ent, PRINT_HIGH, "That player number is not connected.\n");
		return;
	}

	sprintf(text, "kick %d\n", i - 1);
	gi.AddCommandString(text);
}

void
CTFSetPowerUpEffect(edict_t *ent, int def)
{
	if (ent->client->resp.ctf_team == CTF_TEAM1)
	{
		ent->s.effects |= EF_PENT; /* red */
	}
	else if (ent->client->resp.ctf_team == CTF_TEAM2)
	{
		ent->s.effects |= EF_QUAD; /* red */
	}
	else
	{
		ent->s.effects |= def;
	}
}

