/*
 * Copyright (c) 2016-2021 Marco Cawthorne <marco@icculus.org>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
 * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
 * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

string
CSMultiplayerRules::Title(void)
{
	return "Counter-Strike";
}

int
CSMultiplayerRules::MaxItemPerSlot(int slot)
{
	/* grenades */
	if (slot == 3) {
		return (3);
	}
	return (1);
}

void
CSMultiplayerRules::PlayerDisconnect(NSClientPlayer pl)
{
	if (health > 0)
		PlayerDeath(pl);

	NSGameRules::PlayerDisconnect(pl);
}

void
CSMultiplayerRules::PlayerDeath(NSClientPlayer pl)
{
	player targ = (player)g_dmg_eTarget;
	player attk = (player)g_dmg_eAttacker;

	FX_Corpse_Spawn(targ, ANIM_DEATH1);

	/* obituary networking */
	WriteByte(MSG_MULTICAST, SVC_CGAMEPACKET);
	WriteByte(MSG_MULTICAST, EV_OBITUARY);
	if (g_dmg_eAttacker.netname)
		WriteString(MSG_MULTICAST, strcat(HUD_GetChatColorHEX(g_dmg_eAttacker.team), g_dmg_eAttacker.netname));
	else
		WriteString(MSG_MULTICAST, g_dmg_eAttacker.classname);

	WriteString(MSG_MULTICAST, strcat(HUD_GetChatColorHEX(targ.team), targ.netname));

	WriteByte(MSG_MULTICAST, g_dmg_iWeapon);
	WriteByte(MSG_MULTICAST, 0);
	msg_entity = world;
	multicast([0,0,0], MULTICAST_ALL);

	Plugin_PlayerObituary(g_dmg_eAttacker, g_dmg_eTarget, g_dmg_iWeapon, g_dmg_iHitBody, g_dmg_iDamage);

	/* death-counter */
	targ.deaths++;
	forceinfokey(targ, "*deaths", ftos(targ.deaths));

	/* update score-counter */
	if (g_dmg_eTarget.flags & FL_CLIENT || g_dmg_eTarget.flags & FL_MONSTER)
	if (g_dmg_eAttacker.flags & FL_CLIENT) {
		float vip = (g_dmg_eTarget.team == TEAM_VIP && g_dmg_eAttacker.team == TEAM_CT);

		if (g_dmg_eTarget == g_dmg_eAttacker) {
			g_dmg_eAttacker.frags--;
		} else if (g_dmg_eTarget.team == g_dmg_eAttacker.team || vip) {
			g_dmg_eAttacker.frags--;
			Money_AddMoney((NSClientPlayer)g_dmg_eAttacker, autocvar_fcs_penalty_teamkill);
		} else {
			g_dmg_eAttacker.frags++;
			Money_AddMoney((NSClientPlayer)g_dmg_eAttacker, autocvar_fcs_reward_kill);
		}
	}

	Weapon_DropCurrentWeapon(targ);

	/* if we're the bomb carrier, make sure we drop the bomb. */
	if (targ.g_items & ITEM_C4BOMB) {
		targ.activeweapon = WEAPON_C4BOMB;
		Weapon_DropCurrentWeapon(targ);
	} else {
		targ.activeweapon = Cstrike_WeaponToDropUponDeath(targ);
		Weapon_DropCurrentWeapon(targ);
	}

	/* clear all ammo and inventory... */
	PlayerClearWeaponry(targ);
	targ.Death();
	targ.gflags &= ~GF_FLASHLIGHT;

	/* gamerule stuff */
	targ.MakeTempSpectator();
	forceinfokey(targ, "*dead", "1"); 
	forceinfokey(targ, "*team", ftos(targ.team));
	CountPlayers();

	/* In Assassination, all Terrorists receive a $2500
	 *  reward if they won by killing the VIP. */
	if (targ.team == TEAM_VIP) {
		RoundOver(TEAM_T, 2500, FALSE);
		return;
	}
	DeathCheck(targ);
}

void
CSMultiplayerRules::PlayerPreFrame(NSClientPlayer pl)
{
	super::PlayerPreFrame(pl);

	/* the messages that get printed when you aim at something
	   happen here */
	{
		vector sourcePos, destPos;
		player p = (player)pl;

		sourcePos = pl.GetEyePos();
		destPos = sourcePos + (pl.GetForward() * 512);
		traceline(sourcePos, destPos, MOVE_NORMAL, pl);

		if (trace_ent) {
			if (trace_ent.classname == "player")
				if (trace_ent.team == p.team && p.m_seenFriend == false) {
					env_message_single(pl, "Hint_spotted_a_friend");
					p.m_seenFriend = true;
				} else if (trace_ent.team != p.team && p.m_seenEnemy == false) {
					env_message_single(pl, "Hint_spotted_an_enemy");
					p.m_seenEnemy = true;
				}

			if (trace_ent.classname == "hostage_entity" && p.m_seenHostage == false) {
				if (p.team == TEAM_T) {
					env_message_single(pl, "Hint_prevent_hostage_rescue");
				} else {
					env_message_single(pl, "Hint_rescue_the_hostages");
					env_message_single(pl, "Hint_press_use_so_hostage_will_follow");
				}
				p.m_seenHostage = true;
			}
		}
	}
}

void
CSMultiplayerRules::FrameStart(void)
{
	if ((g_total_players > 0) && (g_cs_gamestate == GAME_INACTIVE)) {
		TimerBegin(2, GAME_COMMENCING);
	} else if (g_total_players == 0) {
		g_cs_gamestate = GAME_INACTIVE;
		g_cs_gametime = 0;
		g_cs_roundswon_t = 0;
		g_cs_roundswon_ct = 0;
		g_cs_roundsplayed = 0;
	} else {
		TimerUpdate(); // Timer that happens once players have started joining
	}
}

void
CSMultiplayerRules::CreateRescueZones(void)
{
	int zones = 0;

	/* not in hostage rescue mode */
	if (g_cs_hostagestotal <= 0) {
		return;
	}

	/* count the already existing rescue zones. */
	for (entity e = world; (e = find(e, ::classname, "func_hostage_rescue"));) {
		zones++;
	}

	/* we don't need to create any additional rescue zones. */
	if (zones > 0)
		return;

	/* hostage zones need to go somewhere */
	for (entity e = world; (e = find(e, ::classname, "info_player_start"));) {
		info_hostage_rescue newzone = spawn(info_hostage_rescue, origin: e.origin);
		newzone.Respawn();
	}
}

void
CSMultiplayerRules::CreateCTBuyzones(void)
{
	int zones = 0;

	/* count the already existing CT zones. */
	for (entity e = world; (e = find(e, ::classname, "func_buyzone"));) {
		if (e.team == 0 || e.team == TEAM_CT) {
			zones++;
		}
	}

	/* we don't need to create any additional CT zones. */
	if (zones > 0)
		return;

	/* since no buyzones are available, let's create one around every CT spawn */
	for (entity e = world; (e = find(e, ::classname, "info_player_start"));) {
		info_buyzone newzone = spawn(info_buyzone, origin: e.origin);
		newzone.Respawn();
		newzone.team = TEAM_CT;
	}
}

void
CSMultiplayerRules::CreateTBuyzones(void)
{
	int zones = 0;

	/* count the already existing T zones. */
	for (entity e = world; (e = find(e, ::classname, "func_buyzone"));) {
		if (e.team == 0 || e.team == TEAM_T) {
			zones++;
		}
	}

	/* we don't need to create any additional T zones. */
	if (zones > 0)
		return;

	/* since no buyzones are available, let's create one around every T spawn */
	for (entity e = world; (e = find(e, ::classname, "info_player_deathmatch"));) {
		info_buyzone newzone = spawn(info_buyzone, origin: e.origin);
		newzone.Respawn();
		newzone.team = TEAM_T;
	}
}

void
CSMultiplayerRules::InitPostEnts(void)
{
	MOTD_LoadDefault();

	/* let's check if we need to create buyzones */
	switch (g_cstrike_buying) {
	case BUY_CT:
		CreateCTBuyzones();
		break;
	case BUY_T:
		CreateTBuyzones();
		break;
	case BUY_NEITHER:
		break;
	default:
		CreateCTBuyzones();
		CreateTBuyzones();
	}

	CreateRescueZones();

	/* get all of the vip zones */
	g_cs_vipzones = 0;
	for (entity e = world; (e = find(e, ::classname, "func_vip_safetyzone"));) {
		g_cs_vipzones++;
	}
}

void
CSMultiplayerRules::TimerBegin(float tleft, int mode)
{
	g_cs_gametime = tleft;

	if (mode == GAME_FREEZE) {
		g_cs_gamestate = GAME_FREEZE;
	} else if (mode == GAME_ACTIVE) {
		g_cs_gamestate = GAME_ACTIVE;
		CountPlayers();

		if (g_cs_total_t <= 1 || g_cs_total_ct <= 1)
			return;

		/* if no players are present in the chosen team, force restart round */
		if ((g_cs_alive_t == 0)) {
			RoundOver(TEAM_CT, 3600, FALSE);
			return;
		} else if (g_cs_alive_ct == 0) {
			RoundOver(TEAM_T, 3600, FALSE);
			return;
		}

	} else if (mode == GAME_END) {
		g_cs_gamestate = GAME_END;
	} else if (mode == GAME_COMMENCING) {
		g_cs_gamestate = GAME_COMMENCING;
	} else if (mode == GAME_OVER) {
		g_cs_gamestate = GAME_OVER;
	}
}

void
CSMultiplayerRules::TimerUpdate(void)
{
	/* if we've got hostages in the map... */
	if (g_cs_hostagestotal > 0) {
		/* and they're all rescued.... */
		if (g_cs_hostagesrescued >= g_cs_hostagestotal) {
			/* CTs win! */
			RoundOver(TEAM_CT, 0, FALSE);
			return;
		}
	}

	// This map has been played enough we think
	if (g_cs_gamestate != GAME_OVER) {
		if (cvar("mp_timelimit") > 0) {
			if (time >= (cvar("mp_timelimit") * 60)) {
				IntermissionStart();
				g_cs_gamestate = GAME_OVER;
			}
		}
	}

	// Okay, this means that timelimit is not the only deciding factor
	if (autocvar_mp_winlimit > 0 && g_cs_gamestate != GAME_OVER) {
		// It really doesn't matter who won. Do some logging perhaps?
		if (g_cs_roundswon_ct == autocvar_mp_winlimit ||
			g_cs_roundswon_t == autocvar_mp_winlimit) {
			IntermissionStart();
		}
	}

	/* INACTIVE means no one is registered as a player */
	if (g_cs_gamestate == GAME_INACTIVE) {
		return;
	}

	/* our continously running down timer */
	g_cs_gametime = bound(0, g_cs_gametime - frametime, g_cs_gametime);

	/* if the round is over or the game is done with... */
	if (g_cs_gamestate == GAME_COMMENCING || g_cs_gamestate == GAME_END) {
		if (g_cs_gametime <= 0) {
			if (g_cs_roundswon_t == 0 && g_cs_roundswon_ct == 0) {
				Money_ResetTeamReward();
				Money_ResetRoundReward();
				RestartRound(TRUE);
			} else {
				if (autocvar_mp_halftime == TRUE && (autocvar_mp_winlimit / 2 == g_cs_roundsplayed)) {
					Money_ResetTeamReward();
					SwitchTeams();
					RestartRound(TRUE);
				} else {
					RestartRound(FALSE);
				}
			}
		}
		return;
	}

	if ((g_cs_gamestate == GAME_ACTIVE) || (g_cs_gamestate == GAME_FREEZE)) {
		if (g_cs_gametime <= 0) {
			if (g_cs_gamestate == GAME_ACTIVE) {
				/* 1.5 will make the T's lose if time runs out no matter what */
				if (autocvar_fcs_fix_bombtimer == TRUE) {
					if (g_cs_bombzones > 0 && g_cs_bombplanted == TRUE) {
						return;
					}
				}
				TimeOut();
				TimerBegin(5, GAME_END); // Round is over, 5 seconds til a new round starts
			} else {
				TimerBegin(autocvar_mp_roundtime * 60, GAME_ACTIVE); // Unfreeze
				Radio_StartMessage();
				CSBot_RoundStart();
			}
		}
	}
}

/*
=================
BuyingPossible

Checks if it is possible for players to buy anything
=================
*/
bool
CSMultiplayerRules::BuyingPossible(NSClientPlayer pl)
{
	if (pl.health <= 0) {
		return (false);
	}

	if (g_cs_gamestate == GAME_ACTIVE) {
		if (((autocvar_mp_roundtime * 60) - g_cs_gametime) > autocvar_mp_buytime) {
			centerprint(pl, sprintf("%d seconds have passed...\nYou can't buy anything now!", autocvar_mp_buytime));
			return (false);
		}
	}

	if (pl.team == TEAM_VIP) {
		centerprint(pl, "You are the VIP...\nYou can't buy anything!\n");
		return (false);
	}

	if (g_cstrike_buying == BUY_NEITHER) {
		centerprint(pl, "Sorry, you aren't meant\nto be buying anything.\n");
		return (false);
	}

	if (g_cstrike_buying != BUY_BOTH) {
		if (g_cstrike_buying == BUY_CT && pl.team == TEAM_T) {
			centerprint(pl, "Terrorists aren't allowed to\nbuy anything on this map!\n");
			return (false);
		} else if (g_cstrike_buying == BUY_T && pl.team == TEAM_CT) {
			centerprint(pl, "CTs aren't allowed to buy\nanything on this map!\n");
			return (false);
		}
	}

	if (!(pl.gflags & GF_BUYZONE)) {
		centerprint(pl, "Sorry, you aren't in a buyzone.\n");
		return (false);
	}

	return (true);
}

void
CSMultiplayerRules::MakeBomber(NSClientPlayer pl)
{
	Weapons_AddItem(pl, WEAPON_C4BOMB, -1);
	env_message_single(pl, "Hint_you_have_the_bomb");
}

void
CSMultiplayerRules::MakeVIP(NSClientPlayer pl)
{
	pl.team = TEAM_VIP;
	PlayerRespawn(pl, pl.team);
	env_message_single(pl, "Hint_you_are_the_vip");
	forceinfokey(pl, "*dead", "2");
	pl.SetModel("models/player/vip/vip.mdl");
}

/*
=================
RestartRound

Loop through all ents and handle them
=================
*/
void
CSMultiplayerRules::RestartRound(int iWipe)
{
	if (autocvar_fcs_swaponround > 0)
	if (m_iSwapTeamRoundCounter >= autocvar_fcs_swaponround) {
		m_iSwapTeamRoundCounter = 0;

		for (entity eFind = world; (eFind = find(eFind, ::classname, "player"));) { 
			player pl = (player)eFind;

			if (pl.team == TEAM_T) {
				pl.team = TEAM_CT; /* temp for CT */
				pl.charmodel += 4;
				pl.health = 0;
			} else if (pl.team == TEAM_VIP || pl.team == TEAM_CT) {
				pl.team = TEAM_T; /* temp for CT */
				pl.charmodel -= 4;
				pl.health = 0;
			}
		}
	}

	// Reset CSBot vars
	CSBot_RestartRound();

	for (entity eFind = world; (eFind = findfloat(eFind, ::team, TEAM_T));) {
		if (!(eFind.flags & FL_CLIENT))
			continue;

		player pl = (player)eFind;

		if (pl.health > 0 && iWipe == FALSE) {
			PlayerRespawn(pl, pl.team);
		} else {
			PlayerMakeSpectator(pl);
			PlayerMakePlayable(pl, pl.charmodel);
		}

		if (iWipe == FALSE) {
			Money_GiveTeamReward(pl);
		} else {
			PlayerReset(pl);
		}
	}
	for (entity eFind = world; (eFind = findfloat(eFind, ::team, TEAM_CT));) {
		if (!(eFind.flags & FL_CLIENT))
			continue;

		player pl = (player)eFind;

		if (pl.health > 0 && iWipe == FALSE) {
			PlayerRespawn(pl, pl.team);
		} else {
			PlayerMakeSpectator(pl);
			PlayerMakePlayable(pl, pl.charmodel);
		}

		if (iWipe == FALSE) {
			Money_GiveTeamReward(pl);
		} else {
			PlayerReset(pl);
		}
	}

	/* respawn the VIP as well, but turn them back into a CT. */
	for (entity eFind = world; (eFind = findfloat(eFind, ::team, TEAM_VIP));) {
		if (!(eFind.flags & FL_CLIENT))
			continue;

		player pl = (player)eFind;
		pl.team = TEAM_CT;

		if (pl.health > 0 && iWipe == FALSE) {
			PlayerRespawn(pl, TEAM_CT);
		} else {
			PlayerMakeSpectator(pl);
			PlayerMakePlayable(pl, pl.charmodel);
		}

		if (iWipe == FALSE) {
			Money_GiveTeamReward(pl);
		} else {
			PlayerReset(pl);
		}
	}

	/* clear the corpses/items/bombs */
	for (entity eFind = world; (eFind = find(eFind, ::classname, "remove_me"));) {
		if (eFind.identity) {
			NSEntity e = (NSEntity)eFind;
			e.Destroy();
		} else {
			remove(eFind);
		}
	}
	for (entity eFind = world; (eFind = find(eFind, ::classname, "tempdecal"));) {
		decal dec = (decal)eFind;
		dec.m_strTexture = "";
		dec.SendFlags = -1;
	}
	for (entity eFind = world; (eFind = find(eFind, ::classname, "item_c4"));) {
		NSEntity e = (NSEntity)eFind;
		e.Destroy();
	}
	// Remove potential bomb backpack model from the world, else bots will go
	// chase a ghost.
	entity e = find(world, ::model, "models/w_backpack.mdl");
	if (e != world) {
		remove(e);
	}

	WriteByte(MSG_MULTICAST, SVC_CGAMEPACKET);
	WriteByte(MSG_MULTICAST, EV_CLEARDECALS);
	msg_entity = world;
	multicast([0,0,0], MULTICAST_ALL);

	// Select a random Terrorist for the bomb, if needed
	if (g_cs_bombzones > 0) {
		int iRandomT = floor(random(1, (float)g_cs_alive_t + 1)); 
		int iPickT = 0;

		for (entity eFind = world; (eFind = find(eFind, ::classname, "player"));) { 
			if (eFind.team == TEAM_T) {
				iPickT++;

				if (iPickT == iRandomT) {
					MakeBomber((player)eFind);
				}
			}
		}
	} 

	// If there is a VIP, select a random CT to be it
	if (g_cs_vipzones > 0) {
		int iRandomCT = floor(random(1, (float)g_cs_alive_ct + 1));
		int iPickCT = 0;

		for (entity eFind = world; (eFind = find(eFind, ::classname, "player"));) { 
			if (eFind.team == TEAM_CT) {
				iPickCT++;
				if (iPickCT == iRandomCT) {
					MakeVIP((player)eFind);
				}
			}
		}
	}

	// Respawn all the entities
	for (entity a = world; (a = findfloat(a, ::identity, 1));) {
		NSEntity caw = (NSEntity)a;
		if (caw.classname != "player")
			caw.Respawn();
	}

	CSBot_BuyStart();
	TimerBegin(autocvar_mp_freezetime, GAME_FREEZE);
	Money_ResetTeamReward();
}

/*
=================
RoundOver

This happens whenever an objective is complete or time is up
=================
*/
void
CSMultiplayerRules::RoundOver(int iTeamWon, int iMoneyReward, int fSilent)
{
	if (g_cs_gamestate != GAME_ACTIVE && g_cs_gamestate != GAME_FREEZE) {
		return;
	}

	if (iTeamWon == TEAM_T) {
		if (fSilent == FALSE) {
			Radio_BroadcastMessage(RADIO_TERWIN);
		}
		g_cs_roundswon_t++;
		m_iSwapTeamRoundCounter++;
	} else if (iTeamWon == TEAM_CT) {
		if (fSilent == FALSE) {
			Radio_BroadcastMessage(RADIO_CTWIN);
		}
		g_cs_roundswon_ct++;
		m_iSwapTeamRoundCounter++;

		/* In Bomb Defusal, if Terrorists were able to plant the bomb
		 * but lose the round, all Terrorists receive an $800 bonus. */
		if (g_cs_bombplanted) {
			Money_QueTeamReward(TEAM_T, 800);
		}
	} else {
		if (fSilent == FALSE) {
			Radio_BroadcastMessage(RADIO_ROUNDDRAW);
		}
	}

	Money_HandleRoundReward(iTeamWon);
	Money_QueTeamReward(iTeamWon, iMoneyReward);
	TimerBegin(5, GAME_END); // Round is over, 5 seconds til a new round starts

	g_cs_hostagesrescued = 0;
	g_cs_bombbeingdefused = 0;
	g_cs_bombplanted = 0;
	g_cs_roundsplayed++;

	forceinfokey(world, "teamscore_1", sprintf("%i", g_cs_roundswon_t));
	forceinfokey(world, "teamscore_2", sprintf("%i", g_cs_roundswon_ct));
}

/*
=================
TimeOut

Whenever mp_roundtime was being counted down to 0
=================
*/
void
CSMultiplayerRules::TimeOut(void)
{
	if (g_cs_vipzones > 0) {
		RoundOver(TEAM_T, 3250, FALSE);
	} else if (g_cs_bombzones > 0) {
		/* In Bomb Defusal, all Counter-Terrorists receive $3250
		 *  if they won running down the time. */
		RoundOver(TEAM_CT, 3250, FALSE);
	} else if (g_cs_hostagestotal > 0) {
		// TODO: Broadcast_Print: Hostages have not been rescued!
		RoundOver(TEAM_T, 3250, FALSE);
	} else {
		RoundOver(0, 0, FALSE);
	}
}

/*
=================
SwitchTeams

Happens rarely
=================
*/
void
CSMultiplayerRules::SwitchTeams(void)
{
	int iCTW, iTW;

	for (entity eFind = world; (eFind = find(eFind, ::classname, "player"));) { 
		player pl = (player)eFind;
		if (pl.team == TEAM_CT) {
			pl.team = TEAM_T;
			pl.charmodel -= 4;
		} else if (pl.team == TEAM_T) {
			pl.team = TEAM_CT;
			pl.charmodel += 4;
		}
		forceinfokey(pl, "*team", ftos(pl.team));
	}

	iCTW = g_cs_roundswon_ct;
	iTW = g_cs_roundswon_t;

	g_cs_roundswon_t = iCTW;
	g_cs_roundswon_ct = iTW;

	iCTW = g_cs_alive_ct;
	iTW = g_cs_alive_t;

	g_cs_alive_ct = iTW;
	g_cs_alive_t = iCTW;

	forceinfokey(world, "teamscore_1", sprintf("%i", g_cs_roundswon_t));
	forceinfokey(world, "teamscore_2", sprintf("%i", g_cs_roundswon_ct));
}

void
CSMultiplayerRules::CountPlayers(void)
{
	g_cs_alive_t = 0;
	g_cs_alive_ct = 0;
	g_cs_total_t = 0;
	g_cs_total_ct = 0;

	for (entity eFind = world; (eFind = find(eFind, ::classname, "player"));) {
		if (eFind.team == TEAM_T) {
			g_cs_total_t++;

			if (eFind.health > 0)
				g_cs_alive_t++;
		} else if (eFind.team == TEAM_CT || eFind.team == TEAM_VIP) {
			g_cs_total_ct++;

			if (eFind.health > 0)
				g_cs_alive_ct++;
		}
	}

	g_total_players = g_cs_total_t + g_cs_total_ct;
}

void
CSMultiplayerRules::DeathCheck(NSClientPlayer pl)
{
	if (g_cs_gamestate == GAME_END)
		return;

	/* hack so that we can kill rounds */
	if ((g_cs_alive_t == 0) && (g_cs_alive_ct == 0)) {
		g_cs_gamestate = GAME_ACTIVE;
	}

	switch (g_cs_gamestate) {
	case GAME_INACTIVE:
	case GAME_COMMENCING:
	case GAME_END:
	case GAME_OVER:
		return;
		break;
	}

	if ((g_cs_alive_t == 0) && (g_cs_alive_ct == 0)) {
		if (g_cs_bombplanted == TRUE) {
			RoundOver(TEAM_T, 3600, FALSE);
		} else {
			RoundOver(FALSE, 0, FALSE);
		}
	} else {
		int winner;
		if ((pl.team == TEAM_T) && (g_cs_alive_t == 0)) {
			winner = TEAM_CT;
		} else if ((pl.team == TEAM_CT) && (g_cs_alive_ct == 0)) {
			winner = TEAM_T;
		} else {
			return;
		}

		if (g_cs_bombzones > 0) {
			/* In Bomb Defusal, the winning team receives $3250
			 * if they won by eliminating the enemy team. */
			if (!g_cs_bombplanted || g_cs_alive_ct == 0) {
				RoundOver(winner, 3250, FALSE);
			}
		} else {
			/* In Hostage Rescue, the winning team receives $3600
			 * if they won by eliminating the enemy team. */
			RoundOver(winner, 3600, FALSE);
		}
	}
}

/*
=================
PlayerFindSpawn

Recursive function that gets the next spawnpoint
=================
*/
entity
CSMultiplayerRules::PlayerFindSpawn(float t)
{
	entity point = world;

	if (t == TEAM_T) {
		m_eLastTSpawn = find(m_eLastTSpawn, ::classname, "info_player_deathmatch");

		if (m_eLastTSpawn == world) {
			m_eLastTSpawn = find(m_eLastTSpawn, ::classname, "info_player_deathmatch");
		}
		point = m_eLastTSpawn;
	} else if (t == TEAM_CT) {
		m_eLastCTSpawn = find(m_eLastCTSpawn, ::classname, "info_player_start");

		if (m_eLastCTSpawn == world) {
			m_eLastCTSpawn = find(m_eLastCTSpawn, ::classname, "info_player_start");
		}
		point = m_eLastCTSpawn;
	} else if (t == TEAM_VIP) {
		point = find(world, ::classname, "info_vip_start");

		if (!point)
			point = find(m_eLastTSpawn, ::classname, "info_player_start");
	}

	if (point == world) {
		error("Error: No valid spawnpoints available.");
	}

	return point;
}

/*
=================
PlayerRespawn

Called whenever a player survived a round and needs a basic respawn.
=================
*/
void
CSMultiplayerRules::PlayerRespawn(NSClientPlayer pp, int fTeam)
{
	player pl = (player)pp;

	entity eSpawn;
	forceinfokey(pl, "*spec", "0");
	eSpawn = PlayerFindSpawn(pl.team);

	pl.classname = "player";
	pl.health = pl.max_health = 100;
	forceinfokey(pl, "*dead", "0");
	CountPlayers();

	pl.takedamage = DAMAGE_YES;
	pl.solid = SOLID_SLIDEBOX;
	pl.movetype = MOVETYPE_WALK;
	pl.flags = FL_CLIENT;
	pl.iBleeds = TRUE;
	pl.viewzoom = 1.0;
	pl.g_items &= ~ITEM_C4BOMB;

	pl.SetOrigin(eSpawn.origin);
	pl.angles = eSpawn.angles;
	pl.SendFlags = UPDATE_ALL;
	Client_FixAngle(pl, pl.angles);

	switch (pl.charmodel) {
	case 1:
		pl.model = "models/player/terror/terror.mdl";
		break;
	case 2:
		pl.model = "models/player/leet/leet.mdl";
		break;
	case 3:
		pl.model = "models/player/arctic/arctic.mdl";
		break;
	case 4:
		pl.model = "models/player/guerilla/guerilla.mdl";
		break;
	case 5:
		pl.model = "models/player/urban/urban.mdl";
		break;
	case 6:
		pl.model = "models/player/gsg9/gsg9.mdl";
		break;
	case 7:
		pl.model = "models/player/sas/sas.mdl";
		break;
	case 8:
		pl.model = "models/player/gign/gign.mdl";
		break;
	default:
		pl.model = "models/player/vip/vip.mdl";
	}

	pl.SetModel(pl.model);
	pl.SetSize(VEC_HULL_MIN, VEC_HULL_MAX);

	pl.velocity = [0,0,0];
	pl.progress = 0.0f;
	Weapons_SwitchBest(pl);
	Ammo_AutoFill(pl);
}

void
CSMultiplayerRules::PlayerClearWeaponry(NSClientPlayer pp)
{
	player pl = (player)pp;

	pl.g_items = 0x0;
	pl.activeweapon = 0;
	pl.ammo_50ae = 0;
	pl.ammo_762mm = 0;
	pl.ammo_556mm = 0;
	pl.ammo_556mmbox = 0;
	pl.ammo_338mag = 0;
	pl.ammo_9mm = 0;
	pl.ammo_buckshot = 0;
	pl.ammo_45acp = 0;
	pl.ammo_357sig = 0;
	pl.ammo_57mm = 0;
	pl.ammo_hegrenade = 0;
	pl.ammo_fbgrenade = 0;
	pl.ammo_smokegrenade = 0;
	pl.usp45_mag = 0;
	pl.glock18_mag = 0;
	pl.deagle_mag = 0;
	pl.p228_mag = 0;
	pl.elites_mag = 0;
	pl.fiveseven_mag = 0;
	pl.m3_mag = 0;
	pl.xm1014_mag = 0;
	pl.mp5_mag = 0;
	pl.p90_mag = 0;
	pl.ump45_mag = 0;
	pl.mac10_mag = 0;
	pl.tmp_mag = 0;
	pl.ak47_mag = 0;
	pl.sg552_mag = 0;
	pl.m4a1_mag = 0;
	pl.aug_mag = 0;
	pl.scout_mag = 0;
	pl.awp_mag = 0;
	pl.g3sg1_mag = 0;
	pl.sg550_mag = 0;
	pl.para_mag = 0;
	pl.viewzoom = 1.0f;
	pl.mode_temp = 0;
}

/*
=================
PlayerMakePlayable

Called whenever need a full-reinit of a player.
This may be after a player had died or when the game starts for the first time.
=================
*/

static void
MakePlayable(entity targ)
{
	entity oself = self;
	self = targ;

	if (clienttype(targ) != CLIENTTYPE_REAL)
		spawnfunc_csbot();
	else
		spawnfunc_player();

	self = oself;
}

void
CSMultiplayerRules::PlayerMakePlayable(NSClientPlayer pp, int chara)
{
	player pl = (player)pp;
	/* spectator */
	if (chara == 0) {
		PlayerSpawn(pl);
		return;
	}

	MakePlayable(pp);
	pl.g_items |= ITEM_SUIT;
	Weapons_AddItem(pl, WEAPON_KNIFE, -1);

	/* terrorists */
	if (chara < 5) {
		pl.team = TEAM_T;
		if (autocvar_fcs_knifeonly == FALSE) {
			Weapons_AddItem(pl, WEAPON_GLOCK18, -1);
			pl.ammo_9mm = 40;
		}
	} else {
		pl.team = TEAM_CT;

		if (autocvar_fcs_knifeonly == FALSE) {
			Weapons_AddItem(pl, WEAPON_USP45, -1);
			pl.ammo_45acp = 24;
		}
	}

	pl.ingame = TRUE;
	forceinfokey(pl, "*team", ftos(pl.team)); 
	PlayerRespawn(pl, pl.team);
}

/*
=================
PlayerMakeSpectator

Force the player to become an observer.
=================
*/
void
CSMultiplayerRules::PlayerMakeSpectator(NSClientPlayer pp)
{
	player pl = (player)pp;
	pl.MakeTempSpectator();
	PlayerClearWeaponry(pl);
	pl.view_ofs = g_vec_null;
}

/*
=================
PlayerReset

Called when we give the initial starting money, and also when
another player joins and causes the game rules/scores to reset fully
=================
*/
void
CSMultiplayerRules::PlayerReset(NSClientPlayer pl)
{
	player p = (player)pl;

	/* give the initial server-joining money */
	p.money = 0;
	Money_AddMoney(pl, autocvar_mp_startmoney);
	p.m_buyMessage = false; /* unset the buy message. */
	p.m_hostMessageT = false;
	p.m_seenFriend = false;
	p.m_seenEnemy = false;
	p.m_seenHostage = false;
}

/*
=================
PlayerSpawn

Called on the client first joining the server.
=================
*/
void
CSMultiplayerRules::PlayerSpawn(NSClientPlayer pl)
{
	/* immediately put us into spectating mode */
	PlayerMakeSpectator(pl);
	Spawn_ObserverCam(pl);

	PlayerReset(pl);

	/* we don't belong to any team */
	pl.team = TEAM_SPECTATOR;
	forceinfokey(pl, "*team", "0"); 
}

bool
CSMultiplayerRules::ConsoleCommand(NSClientPlayer pp, string cmd)
{
	tokenize(cmd);

	switch (argv(0)) {
	default:
		return (false);
	}

	return (true);
}

bool
CSMultiplayerRules::IsTeamplay(void)
{
	return true;
}

void
CSMultiplayerRules::CSMultiplayerRules(void)
{
	forceinfokey(world, "teams", "2");
	forceinfokey(world, "team_1", "Terrorist");
	forceinfokey(world, "teamscore_1", "0");
	forceinfokey(world, "teamcolor_1", "1 0 0" );
	forceinfokey(world, "team_2", "Counter-Terrorist");
	forceinfokey(world, "teamscore_2", "0");
	forceinfokey(world, "teamcolor_2", "0.25 0.25 1" );
	m_iEscapedTerrorists = 0;
}

/*
=================
CSEv_JoinTeam_f

Event Handling, called by the Client codebase via 'sendevent'
=================
*/
void
CSEv_JoinTeam_f(float flChar)
{
	CSMultiplayerRules rules;
	player pl;

	/* matches Game_InitRules() */
	if (cvar("sv_playerslots") == 1 || cvar("coop") == 1) {
		return;
	}

	rules = (CSMultiplayerRules)g_grMode;
	pl = (player)self;

	if (pl.team == TEAM_VIP) {
		centerprint(pl, "You are the VIP!\nYou cannot switch roles now.\n");
		return;
	}

	// alive and are trying to switch teams, so subtract us from the Alive_Team counter.
	if (pl.health > 0) {
		rules.PlayerKill(pl);
	}

	switch (g_cs_gamestate) {
	/* spawn the players immediately when its in the freeze state */
	case GAME_FREEZE:
		pl.charmodel = (int)flChar;
		rules.PlayerMakePlayable(pl, (int)flChar);

		if ((pl.team == TEAM_T) && (g_cs_alive_t == 1)) {
			if (g_cs_bombzones > 0) {
				rules.MakeBomber(pl);
			}
		} else if ((pl.team == TEAM_CT) && (g_cs_alive_ct == 1)) {
			if (g_cs_vipzones > 0) {
				rules.MakeVIP(pl);
			}
		}

		break;
	/* otherwise, just prepare their fields for the next round */
	default:
		if (flChar == 0) {
			rules.PlayerSpawn(pl);
			return;
		}

		//PlayerMakeSpectator(pl);
		pl.charmodel = (int)flChar;
		forceinfokey(pl, "*dead", "1");
		break;
	}

	if (flChar < 5)
		pl.team = TEAM_T;
	else
		pl.team = TEAM_CT;

	forceinfokey(pl, "*team", ftos(pl.team)); 

	pl.frags = 0;
	pl.deaths = 0;
	forceinfokey(pl, "*deaths", ftos(pl.deaths));

	rules.CountPlayers();

	/* if no players are present in the chosen team, force restart round */
	if ((pl.team == TEAM_T) && (g_cs_alive_t == 0)) {
		rules.RoundOver(FALSE, 0, FALSE);
	} else if ((pl.team == TEAM_CT) && (g_cs_alive_ct == 0)) {
		rules.RoundOver(FALSE, 0, FALSE);
	}
}

void
CSEv_JoinAuto(void)
{
	CSMultiplayerRules rules;

	/* matches Game_InitRules() */
	if (cvar("sv_playerslots") == 1 || cvar("coop") == 1) {
		return;
	}

	rules = (CSMultiplayerRules)g_grMode;
	rules.CountPlayers();

	if (g_cs_total_ct >= g_cs_total_t) {
		CSEv_JoinTeam_f(floor(random(1,5)));
	} else {
		CSEv_JoinTeam_f(floor(random(5,9)));
	}
}

void
CSEv_JoinSpectator(void)
{
	NSClientPlayer pl = (NSClientPlayer)self;
	ClientKill();
	pl.MakeSpectator();
}
