#pragma semicolon 1
#pragma newdecls required

#include <sourcemod>
#include <sdkhooks>
#include <sdktools>
#include <SteamWorks>
#include <tf2>
#include <tf2_stocks>
#include <tf2items>
#include <tf2attributes>
#include <morecolors>

/*=================================
=            Constants            =
=================================*/

#define PLUGIN_NAME	   "TF2 Tower Defense"
#define PLUGIN_AUTHOR  "floube, benedevil, hurpdurp, dragonisser"
#define PLUGIN_DESC	   "Stop enemies from crossing a map by buying towers and building up defenses."
#define PLUGIN_VERSION "2.1.3"
#define PLUGIN_URL	   "https://github.com/tf2td/towerdefense"
#define PLUGIN_PREFIX  "[TF2TD]"

/*==========================================
=            Plugin Information            =
==========================================*/
public Plugin myinfo =
{
	name		= PLUGIN_NAME,
	author		= PLUGIN_AUTHOR,
	description = PLUGIN_DESC,
	version		= PLUGIN_VERSION,
	url			= PLUGIN_URL
};

/*=======================================
=            Custom Includes            =
=======================================*/

#include "towerdefense/info/constants.sp"
#include "towerdefense/info/enums.sp"
#include "towerdefense/info/variables.sp"
#include "towerdefense/info/convars.sp"

#include "towerdefense/util/log.sp"
#include "towerdefense/util/metal.sp"
#include "towerdefense/util/steamid.sp"
#include "towerdefense/util/tf2items.sp"
#include "towerdefense/util/zones.sp"

#include "towerdefense/handler/antiair.sp"
#include "towerdefense/handler/aoe.sp"
#include "towerdefense/handler/buttons.sp"
#include "towerdefense/handler/corners.sp"
#include "towerdefense/handler/metalpacks.sp"
#include "towerdefense/handler/panels.sp"
#include "towerdefense/handler/player.sp"
#include "towerdefense/handler/server.sp"
#include "towerdefense/handler/sounds.sp"
#include "towerdefense/handler/towers.sp"
#include "towerdefense/handler/waves.sp"
#include "towerdefense/handler/weapons.sp"

#include "towerdefense/database/general.sp"
#include "towerdefense/database/player.sp"
#include "towerdefense/database/server.sp"

#include "towerdefense/commands.sp"
#include "towerdefense/events.sp"
#include "towerdefense/timers.sp"

/*=======================================
=            Public Forwards            =
=======================================*/
public APLRes AskPluginLoad2(Handle hMyself, bool bLate, char[] sError, int iMaxLength) {
	if (GetEngineVersion() != Engine_TF2) {
		Format(sError, iMaxLength, "This mod can only run under TF2.");
		return APLRes_Failure;
	}

	if (SQL_CheckConfig("towerdefense"))
		Database.Connect(ConnectToDB, "towerdefense");
	else {
		Format(sError, iMaxLength, "Unable to read database info from file");
		return APLRes_Failure;
	}

	MarkNativeAsOptional("SteamWorks_SetGameDescription");
	MarkNativeAsOptional("GetUserMessageType");
	return APLRes_Success;
}

public void OnPluginStart() {
	PrintToServer("%s Loaded %s %s by %s", PLUGIN_PREFIX, PLUGIN_NAME, PLUGIN_VERSION, PLUGIN_AUTHOR);

	LoadTranslations("towerdefense.phrases.txt");

	Log_Initialize(TDLogLevel_Trace, TDLogType_Console);

	CreateDataMap(g_hMapTowers);
	CreateDataMap(g_hMapWeapons);
	CreateDataMap(g_hMapWaves);
	CreateDataMap(g_hMapMetalpacks);
	CreateDataMap(g_hMultiplierType);
	CreateDataMap(g_hMultiplier);

	CreateDataMap(g_hServerData);
	CreateDataMap(g_hPlayerData);

	HookEvents();
	RegisterCommands();
	CreateConVars();
	LoadConVars();
	SetConVars();

	SetPassword("WaitingForServerToInitialize", false);
}

public void OnPluginEnd() {
	if (g_bSteamWorks) {
		SteamWorks_SetGameDescription("Team Fortress");
	}

	if (g_hDatabase != null) {
		CloseHandle(g_hDatabase);
		g_hDatabase = null;
	}

	if (g_hMapTowers != null) {
		CloseHandle(g_hMapTowers);
		g_hMapTowers = null;
	}

	if (g_hMapWeapons != null) {
		CloseHandle(g_hMapWeapons);
		g_hMapWeapons = null;
	}

	if (g_hMapWaves != null) {
		CloseHandle(g_hMapWaves);
		g_hMapWaves = null;
	}

	if (g_hMapMetalpacks != null) {
		CloseHandle(g_hMapMetalpacks);
		g_hMapMetalpacks = null;
	}

	if (g_hMultiplierType != null) {
		CloseHandle(g_hMultiplierType);
		g_hMultiplierType = null;
	}

	if (g_hMultiplier != null) {
		CloseHandle(g_hMultiplier);
		g_hMultiplier = null;
	}

	if (g_hServerData != null) {
		CloseHandle(g_hServerData);
		g_hServerData = null;
	}

	if (g_hPlayerData != null) {
		CloseHandle(g_hPlayerData);
		g_hPlayerData = null;
	}

	FindConVar("sv_cheats").SetInt(0, true, false);
}

public void OnMapStart() {
	g_bTowerDefenseMap = IsTowerDefenseMap();

	PrecacheModels();
	PrecacheSounds();

	g_iHealthBar	   = GetHealthBar();

	g_bConfigsExecuted = false;
}

public void OnMapEnd() {
	g_bMapRunning	 = false;

	bool bWasEnabled = g_bEnabled;
	g_bEnabled		 = false;

	if (bWasEnabled) {
		UpdateGameDescription();
	}

	AddConVarFlag("sv_cheats", FCVAR_NOTIFY);
	AddConVarFlag("sv_tags", FCVAR_NOTIFY);
	AddConVarFlag("tf_bot_count", FCVAR_NOTIFY);
	AddConVarFlag("sv_password", FCVAR_NOTIFY);
}

public void OnConfigsExecuted() {
	g_bEnabled	  = g_hEnabled.BoolValue && g_bTowerDefenseMap && g_bSteamWorks && g_bTF2Attributes;
	g_bMapRunning = true;

	UpdateGameDescription();

	if (!g_bEnabled) {
		if (!g_bTowerDefenseMap) {
			char sCurrentMap[PLATFORM_MAX_PATH];
			GetCurrentMap(sCurrentMap, sizeof(sCurrentMap));

			Log(TDLogLevel_Info, "Map \"%s\" is not supported, Tower Defense has been disabled.", sCurrentMap);
			SetPassword("", false);

		} else {
			Log(TDLogLevel_Info, "Tower Defense is disabled.");
		}
		return;
	}

	g_bServerInitialized = false;

	CreateTimer(5.0, InitializeDelay, INVALID_HANDLE, TIMER_FLAG_NO_MAPCHANGE);
}

public Action InitializeDelay(Handle hTimer, any iTime) {
	Server_Initialize();
	return Plugin_Stop;
}

public void OnAllPluginsLoaded() {
	g_bSteamWorks = LibraryExists("SteamWorks");

	if (g_bSteamWorks) {
		UpdateGameDescription();

		Log(TDLogLevel_Debug, "Found SteamWorks on startup");
	}

	g_bTF2Attributes = LibraryExists("tf2attributes");

	if (g_bTF2Attributes) {
		Log(TDLogLevel_Debug, "Found TF2Attributes on startup");
	}
}

public void OnLibraryAdded(const char[] sName) {
	if (StrEqual(sName, "SteamWorks", false)) {
		g_bSteamWorks = true;
		UpdateGameDescription();

		Log(TDLogLevel_Debug, "SteamWorks loaded");
	} else if (StrEqual(sName, "tf2attributes", false)) {
		g_bTF2Attributes = true;

		Log(TDLogLevel_Debug, "TF2Attributes loaded");
	}
}

public void OnLibraryRemoved(const char[] sName) {
	if (StrEqual(sName, "SteamWorks", false)) {
		g_bSteamWorks = false;

		Log(TDLogLevel_Debug, "SteamWorks unloaded");
	} else if (StrEqual(sName, "tf2attributes", false)) {
		g_bTF2Attributes = false;

		Log(TDLogLevel_Debug, "TF2Attributes unloaded");
	}
}

public void OnClientPutInServer(int iClient) {
	for (int i = 0; i < sizeof(g_fBeamPoints[]); i++) {
		g_fBeamPoints[iClient][i][0] = -1.0;
		g_fBeamPoints[iClient][i][1] = -1.0;
		g_fBeamPoints[iClient][i][2] = -1.0;
	}
}

public void OnClientPostAdminCheck(int iClient) {
	if (!g_bEnabled) {
		return;
	}

	char sSteamId[32];
	GetClientAuthId(iClient, AuthId_Steam2, sSteamId, sizeof(sSteamId));

	if (!StrEqual(sSteamId, "BOT")) {
		char sCommunityId[32];
		GetClientCommunityId(iClient, sCommunityId, sizeof(sCommunityId));

		char sIp[32];
		GetClientIP(iClient, sIp, sizeof(sIp));

		Player_Connected(GetClientUserId(iClient), iClient, GetClientNameShort(iClient), sSteamId, sCommunityId, sIp);
	}

	SDKHook(iClient, SDKHook_OnTakeDamage, OnTakeDamage);
	SDKHook(iClient, SDKHook_OnTakeDamagePost, OnTakeDamagePost);
}

public void OnClientDisconnect(int iClient) {
	if (!g_bEnabled) {
		return;
	}

	if (IsTower(g_iAttachedTower[iClient])) {
		Tower_OnCarrierDisconnected(g_iAttachedTower[iClient], iClient);
	}

	if (IsDefender(iClient)) {
		int iMetal = GetClientMetal(iClient);

		if (iMetal > 0) {
			float fLocation[3];

			GetClientEyePosition(iClient, fLocation);
			fLocation[2] = fLocation[2] - GetDistanceToGround(fLocation) + 10.0;

			SpawnMetalPack(TDMetalPack_Medium, fLocation, iMetal);
		}
		UpdateGameDescription();
	}
}

public Action OnPlayerRunCmd(int iClient, int &iButtons, int &iImpulse, float fVelocity[3], float fAngles[3], int &iWeapon) {
	if (!g_bEnabled) {
		return Plugin_Continue;
	}

	// Force towers to shoot
	if (IsTower(iClient)) {
		TDTowerId iTowerId = GetTowerId(iClient);

		// Refill ammo for airblast tower
		if (iTowerId == TDTower_Airblast_Pyro) {
			int iOffset = FindSendPropInfo("CTFPlayer", "m_iAmmo");
			if (GetEntData(iClient, iOffset + 4) <= 0) {
				SetEntData(iClient, iOffset + 4, 100);
			}
		}
		if (iTowerId == TDTower_AoE_Engineer && g_bAoEEngineerAttack) {
			iButtons |= IN_ATTACK;
		}

		if (Tower_GetAttackPrimary(iTowerId)) {
			iButtons |= IN_ATTACK;
		}

		if (Tower_GetAttackSecondary(iTowerId)) {
			iButtons |= IN_ATTACK2;
		}

		if (Tower_GetRotate(iTowerId) && g_bTowersLocked) {
			float fClientEyePosition[3];
			GetClientEyePosition(iClient, fClientEyePosition);

			int iClosest = GetClosestClient(iClient);
			if (!IsValidClient(iClosest) || !IsClientConnected(iClosest) || !IsClientInGame(iClosest))
				return Plugin_Continue;

			float fClosestLocation[3];
			GetClientAbsOrigin(iClosest, fClosestLocation);

			float fVector[3];
			MakeVectorFromPoints(fClosestLocation, fClientEyePosition, fVector);

			float fAngle[3];
			GetVectorAngles(fVector, fAngle);
			fAngle[0] = (fAngle[0] * -1.0) + 355;
			fAngle[1] += 180.0;

			TeleportEntity(iClient, NULL_VECTOR, fAngle, NULL_VECTOR);
		}
	}

	if (IsAttacker(iClient) && g_bBoostWave) {
		fVelocity[0] = 500.0;
	}
	if (IsAttacker(iClient) && g_iSlowAttacker[iClient]) {
		fVelocity[0] = -10000.0;
	}

	if (IsDefender(iClient)) {
		// Attach/detach tower on right-click
		if (IsButtonReleased(iClient, iButtons, IN_ATTACK2)) {
			char sActiveWeapon[64];
			GetClientWeapon(iClient, sActiveWeapon, sizeof(sActiveWeapon));

			if (StrEqual(sActiveWeapon, "tf_weapon_wrench") || StrEqual(sActiveWeapon, "tf_weapon_robot_arm")) {
				if (IsTower(g_iAttachedTower[iClient])) {
					Tower_Drop(iClient);
				} else {
					Tower_Pickup(iClient);
				}
			}
		}

		// Show tower info on left-click
		if (IsButtonReleased(iClient, iButtons, IN_ATTACK)) {
			char sActiveWeapon[64];
			GetClientWeapon(iClient, sActiveWeapon, sizeof(sActiveWeapon));

			if (StrEqual(sActiveWeapon, "tf_weapon_wrench") || StrEqual(sActiveWeapon, "tf_weapon_robot_arm")) {
				Tower_ShowInfo(iClient);
			}
		}

		float fLocation[3], fViewAngles[3];
		GetClientEyePosition(iClient, fLocation);
		GetClientEyeAngles(iClient, fViewAngles);

		TR_TraceRayFilter(fLocation, fViewAngles, MASK_VISIBLE, RayType_Infinite, TraceRayEntities, iClient);

		if (TR_DidHit()) {
			int iAimEntity = TR_GetEntityIndex();

			if (IsValidEntity(iAimEntity)) {
				char sClassname[64];
				GetEntityClassname(iAimEntity, sClassname, sizeof(sClassname));

				if (StrEqual(sClassname, "func_breakable")) {
					char sName[64];
					GetEntPropString(iAimEntity, Prop_Data, "m_iName", sName, sizeof(sName));

					if (StrContains(sName, "break_tower_") != -1) {
						float fEntityLocation[3];
						GetEntPropVector(iAimEntity, Prop_Send, "m_vecOrigin", fEntityLocation);

						if (GetVectorDistance(fLocation, fEntityLocation) <= 512.0) {
							TDTowerId iTowerId;

							if (StrContains(sName, "break_tower_tp_") != -1) {
								char sNameParts[4][32];
								ExplodeString(sName, "_", sNameParts, sizeof(sNameParts), sizeof(sNameParts[]));

								iTowerId = view_as<TDTowerId>(StringToInt(sNameParts[3]));
								Tower_GetName(iTowerId, sName, sizeof(sName));
							} else {
								char sNameParts[3][32];
								ExplodeString(sName, "_", sNameParts, sizeof(sNameParts), sizeof(sNameParts[]));

								iTowerId = view_as<TDTowerId>(StringToInt(sNameParts[2]));
								Tower_GetName(iTowerId, sName, sizeof(sName));
							}

							char sDescription[1024];
							if (Tower_GetDescription(iTowerId, sDescription, sizeof(sDescription))) {
								char sDamagetype[64];
								if (Tower_GetDamagetype(iTowerId, sDamagetype, sizeof(sDamagetype))) {
									PrintToHud(iClient, "\
										%s \n\
										--------------- \n\
										Price: %d metal (%d metal/player)\n\
										Damagetype: %s \n\
										Number of Levels: %d \n\
										--------------- \n\
										%s",
											   sName, Tower_GetPrice(iTowerId), Tower_GetPrice(iTowerId) / GetRealClientCount(true), sDamagetype, Tower_GetMaxLevel(iTowerId), sDescription);
								}
							}
						}
					}
				}
			}
		}
	}

	if (g_bPickupSentry[iClient]) {
		iButtons |= IN_ATTACK2;
		g_bPickupSentry[iClient] = false;
	}

	g_iLastButtons[iClient] = iButtons;

	return Plugin_Continue;
}

public Action OnTakeDamage(int iClient, int &iAttacker, int &iInflictor, float &fDamage, int &iDamageType, int &iWeapon, float fDamageForce[3], float fDamagePosition[3]) {
	if (!g_bEnabled) {
		return Plugin_Continue;
	}

	// Block tower taking damage
	if (IsTower(iClient)) {
		return Plugin_Handled;
	}

	if (IsClientInGame(iClient)) {
		if (!IsValidEdict(iInflictor)) {
			return Plugin_Continue;
		}
		char sAttackerObject[128];
		GetEdictClassname(iInflictor, sAttackerObject, sizeof(sAttackerObject));

		// Sentry Damage
		if (StrEqual(sAttackerObject, "obj_sentrygun")) {
			fDamage *= fMultiplier[Multiplier_GetInt("sentry")] + 1.0;
			// Register Damage For Stats
			if (IsValidClient(iAttacker) && IsDefender(iAttacker)) {
				Player_CAddValue(iAttacker, PLAYER_DAMAGE, RoundToZero(fDamage));
			}
			return Plugin_Changed;
		}

		// Blast Damage
		if (iDamageType & DMG_BLAST) {
			fDamage *= fMultiplier[Multiplier_GetInt("explosion")] + 1.0;
			// Register Damage For Stats
			if (IsValidClient(iAttacker) && IsDefender(iAttacker)) {
				Player_CAddValue(iAttacker, PLAYER_DAMAGE, RoundToZero(fDamage));
			}
			return Plugin_Changed;
		}

		// Fire Damage
		if (iDamageType & DMG_BURN) {
			fDamage *= fMultiplier[Multiplier_GetInt("fire")] + 1.0;
			// Register Damage For stats
			if (IsValidClient(iAttacker) && IsDefender(iAttacker)) {
				Player_CAddValue(iAttacker, PLAYER_DAMAGE, RoundToZero(fDamage));
			}
			return Plugin_Changed;
		}

		// Bullet Damage
		if (iDamageType & DMG_BULLET) {
			fDamage *= fMultiplier[Multiplier_GetInt("bullet")] + 1.0;
			// Register Damage For Stats
			if (IsValidClient(iAttacker) && IsDefender(iAttacker)) {
				Player_CAddValue(iAttacker, PLAYER_DAMAGE, RoundToZero(fDamage));
			}
			return Plugin_Changed;
		}
	}

	if (IsValidClient(iAttacker) && IsDefender(iAttacker)) {
		Player_CAddValue(iAttacker, PLAYER_DAMAGE, RoundToZero(fDamage));
	}

	if (IsDefender(iClient)) {
		if (IsValidClient(iAttacker)) {
			if (iClient == iAttacker || GetClientTeam(iClient) != GetClientTeam(iAttacker)) {
				if (fDamage >= GetClientHealth(iClient)) {
					int iMetal = GetClientMetal(iClient) / 2;

					if (iMetal > 0) {
						float fLocation[3];

						GetClientEyePosition(iClient, fLocation);
						fLocation[2] = fLocation[2] - GetDistanceToGround(fLocation) + 10.0;

						SpawnMetalPack(TDMetalPack_Medium, fLocation, iMetal);
					}
				}
			}
		}
	}

	return Plugin_Continue;
}

public void OnTakeDamagePost(int iClient, int iAttacker, int iInflictor, float fDamage, int iDamageType) {
	if (!g_bEnabled) {
		return;
	}

	if (IsAttacker(iClient)) {
		Wave_OnTakeDamagePost(iClient, iAttacker, iInflictor, fDamage, iDamageType);
	}
}

public void OnEntityCreated(int iEntity, const char[] sClassname) {
	if (StrEqual(sClassname, "tf_ammo_pack") || StrEqual(sClassname, "tf_dropped_weapon")) {
		SDKHook(iEntity, SDKHook_SpawnPost, OnEntitySpawnKill);
	} else if (StrEqual(sClassname, "func_breakable")) {
		SDKHook(iEntity, SDKHook_SpawnPost, OnButtonSpawned);
	} else if (StrEqual(sClassname, "tf_projectile_rocket") || StrEqual(sClassname, "tf_projectile_flare")) {
		SDKHook(iEntity, SDKHook_SpawnPost, OnAntiAirProjectileSpawned);
	} else if (StrContains(sClassname, "tf_projectile_") != -1) {
		SDKHook(iEntity, SDKHook_SpawnPost, OnProjectileSpawned);
	} else if (StrEqual(sClassname, "trigger_multiple")) {
		SDKHook(iEntity, SDKHook_StartTouchPost, Wave_OnTouchCorner);
	}
}

public void OnEntitySpawnKill(int iEntity) {
	AcceptEntityInput(iEntity, "Kill");
}

public void OnButtonSpawned(int iEntity) {
	if (!g_bEnabled) {
		return;
	}

	char sName[64];
	GetEntPropString(iEntity, Prop_Data, "m_iName", sName, sizeof(sName));

	if (StrEqual(sName, "wave_start")) {
		g_iWaveStartButton = iEntity;
		GetEntPropVector(iEntity, Prop_Send, "m_vecOrigin", g_fWaveStartButtonLocation);
	}
}

public void OnAntiAirProjectileSpawned(int iEntity) {
	if (!g_bEnabled) {
		return;
	}

	SDKHook(iEntity, SDKHook_ShouldCollide, OnProjectileCollide);
	int iOwner = GetEntPropEnt(iEntity, Prop_Send, "m_hOwnerEntity");

	if (IsTower(iOwner)) {
		AntiAir_ProjectileTick(iEntity, iOwner);
	}
}

public void OnProjectileSpawned(int iEntity) {
	if (!g_bEnabled) {
		return;
	}

	SDKHook(iEntity, SDKHook_ShouldCollide, OnProjectileCollide);
}

public bool OnProjectileCollide(int iEntity, int iCollisiongroup, int iContentsmask, bool bResult) {
	if (!g_bEnabled) {
		return true;
	}

	int iOwner = GetEntPropEnt(iEntity, Prop_Send, "m_hOwnerEntity");

	if (IsValidEntity(iOwner)) {
		int	  iTeam = GetEntProp(iOwner, Prop_Send, "m_iTeamNum");

		float fLocation[3], fAngles[3];
		GetEntPropVector(iEntity, Prop_Data, "m_vecOrigin", fLocation);
		GetEntPropVector(iEntity, Prop_Data, "m_angAbsRotation", fAngles);

		TR_TraceRayFilter(fLocation, fAngles, MASK_PLAYERSOLID, RayType_Infinite, TraceRayPlayers);

		if (TR_DidHit()) {
			int iTarget = TR_GetEntityIndex();

			if (IsValidClient(iTarget)) {
				if (GetClientTeam(iTarget) != iTeam) {
					SetEntProp(iEntity, Prop_Data, "m_CollisionGroup", 0);
				} else {
					SetEntProp(iEntity, Prop_Data, "m_CollisionGroup", 24);
				}
			}
		}
	}

	return true;
}

public Action OnNobuildEnter(int iEntity, int iClient) {
	if (!g_bEnabled) {
		return Plugin_Continue;
	}

	if (IsDefender(iClient)) {
		g_bInsideNobuild[iClient] = true;
	} else if (IsTower(iClient)) {
		if (IsDefender(g_iLastMover[iClient])) {
			Tower_OnTouchNobuild(iClient);
		}
	}

	return Plugin_Continue;
}

public Action OnNobuildExit(int iEntity, int iClient) {
	if (!g_bEnabled) {
		return Plugin_Continue;
	}

	if (IsDefender(iClient)) {
		g_bInsideNobuild[iClient] = false;
	}

	return Plugin_Continue;
}

/*=========================================
=            Utility Functions            =
=========================================*/

/**
 * Checks if a client is a valid client.
 *
 * @param iClient		The clients index.
 * @return				True on success, false otherwise.
 */

stock bool IsValidClient(int iClient) {
	return (iClient > 0 && iClient <= MaxClients);
}

/**
 * Checks if a client is a defender.
 *
 * @param iClient		The client.
 * @return				True on success, false otherwise.
 */

stock bool IsDefender(int iClient) {
	return (IsValidClient(iClient) && IsClientConnected(iClient) && IsClientInGame(iClient) && !IsFakeClient(iClient) && GetClientTeam(iClient) == TEAM_DEFENDER);
}

/**
 * Checks if a client is a tower.
 *
 * @param iClient		The client.
 * @return				True on success, false otherwise.
 */

stock bool IsTower(int iClient) {
	return (IsValidClient(iClient) && IsClientConnected(iClient) && IsClientInGame(iClient) && IsFakeClient(iClient) && GetClientTeam(iClient) == TEAM_DEFENDER);
}

/**
 * Checks if a client is an attacker.
 *
 * @param iClient		The client.
 * @return				True on success, false otherwise.
 */

stock bool IsAttacker(int iClient) {
	return (IsValidClient(iClient) && IsClientConnected(iClient) && IsClientInGame(iClient) && IsFakeClient(iClient) && GetClientTeam(iClient) == TEAM_ATTACKER);
}

/**
 * Checks if the current map is a Tower Defense map,
 * which can either start with td_ or tf2td_.
 *
 * @return		True if the current map is a Tower Defense map, false otherwise.
 */

stock bool IsTowerDefenseMap() {
	char sCurrentMap[PLATFORM_MAX_PATH];
	GetCurrentMap(sCurrentMap, sizeof(sCurrentMap));

	return (strncmp(sCurrentMap, "td_", 3) == 0 || strncmp(sCurrentMap, "tf2td_", 6) == 0);
}

/**
 * Changes the 'Game' tab in the server browser, according to the plugins state.
 *
 * @noreturn
 */

stock void UpdateGameDescription() {
	if (!g_bSteamWorks) {
		return;
	}

	char sGamemode[64];

	if (g_bEnabled) {
		Format(sGamemode, sizeof(sGamemode), "%s (%s)", GAME_DESCRIPTION, PLUGIN_VERSION);
	} else {
		strcopy(sGamemode, sizeof(sGamemode), "Team Fortress");
	}

	SteamWorks_SetGameDescription(sGamemode);
}

/**
 * Precaches needed models.
 *
 * @noreturn
 */

stock void PrecacheModels() {
	PrecacheModel("models/bots/scout/bot_scout.mdl");
	PrecacheModel("models/bots/sniper/bot_sniper.mdl");
	PrecacheModel("models/bots/soldier/bot_soldier.mdl");
	PrecacheModel("models/bots/demo/bot_demo.mdl");
	PrecacheModel("models/bots/medic/bot_medic.mdl");
	PrecacheModel("models/bots/heavy/bot_heavy.mdl");
	PrecacheModel("models/bots/pyro/bot_pyro.mdl");
	PrecacheModel("models/bots/spy/bot_spy.mdl");
	PrecacheModel("models/bots/engineer/bot_engineer.mdl");

	PrecacheModel("models/items/ammopack_large.mdl");
	PrecacheModel("models/items/ammopack_medium.mdl");
	PrecacheModel("models/items/ammopack_small.mdl");

	g_iLaserMaterial = PrecacheModel("materials/sprites/laserbeam.vmt");
	g_iHaloMaterial	 = PrecacheModel("materials/sprites/halo01.vmt");
}

/**
 * Precaches needed sounds.
 *
 * @noreturn
 */

stock void PrecacheSounds() {
	char sRoboPath[64];
	for (int i = 1; i <= 18; i++) {
		Format(sRoboPath, sizeof(sRoboPath), "mvm/player/footsteps/robostep_%s%i.mp3", (i < 10) ? "0" : "", i);
		PrecacheSound(sRoboPath);
	}

	PrecacheSound("items/ammo_pickup.wav");
	PrecacheSound("items/gunpickup2.wav");
	PrecacheSound("vo/announcer_begins_5sec.mp3");
	PrecacheSound("vo/announcer_begins_4sec.mp3");
	PrecacheSound("vo/announcer_begins_3sec.mp3");
	PrecacheSound("vo/announcer_begins_2sec.mp3");
	PrecacheSound("vo/announcer_begins_1sec.mp3");
	PrecacheSound("vo/mvm_mannup_wave_end01.mp3");
	PrecacheSound("vo/mvm_mannup_wave_end02.mp3");
	PrecacheSound("vo/mvm_wave_end01.mp3");
	PrecacheSound("vo/mvm_wave_end02.mp3");
	PrecacheSound("vo/mvm_wave_end03.mp3");
	PrecacheSound("vo/mvm_wave_end04.mp3");
	PrecacheSound("vo/mvm_wave_end05.mp3");
	PrecacheSound("vo/mvm_wave_end06.mp3");
	PrecacheSound("vo/mvm_wave_end07.mp3");
	PrecacheSound("vo/engineer_no01.mp3");
	PrecacheSound("vo/engineer_no02.mp3");
	PrecacheSound("vo/engineer_no03.mp3");
	PrecacheSound("music/mvm_start_mid_wave.wav");
	PrecacheSound("music/mvm_start_last_wave.wav");
	PrecacheSound("music/mvm_end_last_wave.wav");
}

/**
 * Counts non-fake clients connected to the game.
 *
 * @param bInGameOnly	Only count clients which are in-game.
 * @return				The client count.
 */

stock int GetRealClientCount(bool bInGameOnly = false) {
	int iClients = 0;

	for (int iClient = 1; iClient <= MaxClients; iClient++) {
		if (((bInGameOnly) ? IsClientInGame(iClient) : IsClientConnected(iClient)) && !IsFakeClient(iClient)) {
			iClients++;
		}
	}

	return iClients;
}

/**
 * Checks if a button is being pressed/was pressed.
 *
 * @param iClient		The client.
 * @param iButtons		The clients current buttons.
 * @param iButton		The button to check for.
 * @return				True if pressed, false otherwise.
 */

stock bool IsButtonPressed(int iClient, int iButtons, int iButton) {
	return ((iButtons & iButton) == iButton && (g_iLastButtons[iClient] & iButton) != iButton);
}

/**
 * Checks if a button is being released/was released.
 *
 * @param iClient		The client.
 * @param iButtons		The clients current buttons.
 * @param iButton		The button to check for.
 * @return				True if released, false otherwise.
 */

stock bool IsButtonReleased(int iClient, int iButtons, int iButton) {
	return ((g_iLastButtons[iClient] & iButton) == iButton && (iButtons & iButton) != iButton);
}

/**
 * Get the target a client is aiming at.
 *
 * @param iClient		The client.
 * @return				The targets or -1 if no target is being aimed at.
 */

stock int GetAimTarget(int iClient) {
	float fLocation[3], fAngles[3];
	GetClientEyePosition(iClient, fLocation);
	GetClientEyeAngles(iClient, fAngles);

	TR_TraceRayFilter(fLocation, fAngles, MASK_PLAYERSOLID, RayType_Infinite, TraceRayPlayers, iClient);

	if (TR_DidHit()) {
		int iEntity = TR_GetEntityIndex();

		if (IsValidEntity(iEntity) && IsValidClient(iEntity)) {
			return iEntity;
		}
	}

	return -1;
}

public bool TraceRayPlayers(int iEntity, int iMask, any iData) {
	return (iEntity != iData) && IsValidClient(iEntity);
}

/**
 * Get the entity a client is aiming at.
 *
 * @param iClient		The client.
 * @return				The entity or -1 if no entity is being aimed at.
 */

stock int GetAimEntity(int iClient) {
	float fLocation[3], fAngles[3];
	GetClientEyePosition(iClient, fLocation);
	GetClientEyeAngles(iClient, fAngles);

	TR_TraceRayFilter(fLocation, fAngles, MASK_PLAYERSOLID, RayType_Infinite, TraceRayEntities, iClient);

	if (TR_DidHit()) {
		int iEntity = TR_GetEntityIndex();

		if (IsValidEntity(iEntity)) {
			return iEntity;
		}
	}

	return -1;
}

public bool TraceRayEntities(int iEntity, int iMask, any iData) {
	return (iEntity != iData) && iEntity > MaxClients && IsValidEntity(iEntity);
}

/**
 * Gets a client by name.
 *
 * @param iClient		The executing clients index.
 * @param sName			The clients name.
 * @return				The clients client index, or -1 on error.
 */

stock int GetClientByName(int iClient, char[] sName) {
	char sNameOfIndex[MAX_NAME_LENGTH + 1];
	int	 iLen	  = strlen(sName);

	int	 iResults = 0;
	int	 iTarget  = 0;

	for (int i = 1; i <= MaxClients; i++) {
		if (IsClientInGame(i)) {
			GetClientName(i, sNameOfIndex, sizeof(sNameOfIndex));
			Substring(sNameOfIndex, sizeof(sNameOfIndex), sNameOfIndex, sizeof(sNameOfIndex), 0, iLen);

			if (StrEqual(sName, sNameOfIndex)) {
				iResults++;
				iTarget = i;
			}
		}
	}

	if (iResults == 1) {
		return iTarget;
	} else if (iResults == 0) {
		if (IsDefender(iClient)) {
			Forbid(iClient, true, "%s %t", PLUGIN_PREFIX, "forbidCantFindPlayer", sName);
		}
	} else {
		if (IsDefender(iClient)) {
			Forbid(iClient, true, "%s %t", PLUGIN_PREFIX, "forbidTooUnspecific");
		}
	}

	return -1;
}

/**
 * Gets a substring of an string.
 *
 * @param sDest				The string to copy to.
 * @param iDestLength		The length of the destination string.
 * @param sSource			The string to copy from.
 * @param iSourceLength		The length of the source string.
 * @param iStart			The positon to start the copy from.
 * @param iEnd				The postion to end the copy.
 * @return					True on success, false otherwise.
 */

stock bool Substring(char[] sDest, int iDestLength, char[] sSource, int iSourceLength, int iStart, int iEnd) {
	if (iEnd < iStart || iEnd > (iSourceLength - 1)) {
		strcopy(sDest, iDestLength, NULL_STRING);
		return false;
	} else {
		strcopy(sDest, (iEnd - iStart + 1), sSource[iStart]);
		return true;
	}
}

/**
 * Checks if a string is numeric.
 *
 * @param sText			The string which should be checked.
 * @return				True if number, false otherwise.
 */

stock bool IsStringNumeric(char[] sText) {
	for (int iChar = 0; iChar < strlen(sText); iChar++) {
		if (!IsCharNumeric(sText[iChar])) {
			return false;
		}
	}

	return true;
}

/**
 * Gets the distance between a location and the ground.
 *
 * @param fLocation		The location vector.
 * @return				Distance to the ground.
 */

stock float GetDistanceToGround(float fLocation[3]) {
	float fGround[3];

	TR_TraceRayFilter(fLocation, view_as<float>({ 90.0, 0.0, 0.0 }), MASK_PLAYERSOLID, RayType_Infinite, TraceRayNoPlayers, 0);

	if (TR_DidHit()) {
		TR_GetEndPosition(fGround);

		return GetVectorDistance(fLocation, fGround);
	}

	return 0.0;
}

public bool TraceRayNoPlayers(int iEntity, int iMask, any iData) {
	return !(iEntity == iData || IsValidClient(iEntity));
}

/**
 * Sets the model of a client to the robot model.
 *
 * @param iClient		The client.
 * @noreturn
 */

stock void SetRobotModel(int iClient) {
	char sClass[16], sModelPath[PLATFORM_MAX_PATH];

	GetClientClassName(iClient, sClass, sizeof(sClass));
	Format(sModelPath, sizeof(sModelPath), "models/bots/%s/bot_%s.mdl", sClass, sClass);
	SetVariantString(sModelPath);
	AcceptEntityInput(iClient, "SetCustomModel");
	SetEntProp(iClient, Prop_Send, "m_bUseClassAnimations", 1);
}

/**
 * Gets the name of the class a client is playing.
 *
 * @param iClient 		The client.
 * @param sBuffer		The destination string buffer.
 * @param iMaxLength	The maximum length of the output string buffer.
 * @noreturn
 */

stock void GetClientClassName(int iClient, char[] sBuffer, int iMaxLength) {
	switch (TF2_GetPlayerClass(iClient)) {
		case TFClass_Scout: {
			strcopy(sBuffer, iMaxLength, "scout");
		}
		case TFClass_Sniper: {
			strcopy(sBuffer, iMaxLength, "sniper");
		}
		case TFClass_Soldier: {
			strcopy(sBuffer, iMaxLength, "soldier");
		}
		case TFClass_DemoMan: {
			strcopy(sBuffer, iMaxLength, "demo");
		}
		case TFClass_Medic: {
			strcopy(sBuffer, iMaxLength, "medic");
		}
		case TFClass_Heavy: {
			strcopy(sBuffer, iMaxLength, "heavy");
		}
		case TFClass_Pyro: {
			strcopy(sBuffer, iMaxLength, "pyro");
		}
		case TFClass_Spy: {
			strcopy(sBuffer, iMaxLength, "spy");
		}
		case TFClass_Engineer: {
			strcopy(sBuffer, iMaxLength, "engineer");
		}
	}
}

/**
 * Check if a client is allowed to build an object.
 *
 * @param iClient 		The client.
 * @param iType			The building type.
 * @return				True if allowed, false otherwise.
 */
public bool CanClientBuild(int iClient, TDBuildingType iType) {
	if (!IsValidClient(iClient)) {
		return false;
	}

	if (TF2_IsPlayerInCondition(iClient, TFCond_Taunting)) {
		Forbid(iClient, true, "%s %t", PLUGIN_PREFIX, "forbidNoBuildWhileTaunting");
		return false;
	}

	if (g_bCarryingObject[iClient]) {
		Forbid(iClient, true, "%s %t", PLUGIN_PREFIX, "forbidNoBuildWhileCarrying");
		return false;
	}

	switch (iType) {
		case TDBuilding_Sentry: {
			if (GetClientMetal(iClient) < 130) {
				Forbid(iClient, true, "%s %t", PLUGIN_PREFIX, "forbidInsufficientMetal", 130);
				return false;
			}

			int iEntity = -1, iCount = 0, iOwner = -1;

			while ((iEntity = FindEntityByClassname(iEntity, "obj_sentrygun")) != -1) {
				iOwner = GetEntPropEnt(iEntity, Prop_Send, "m_hBuilder");

				if (iOwner == iClient) {
					iCount++;
				}

				if (iCount > g_iBuildingLimit[TDBuilding_Sentry]) {
					AcceptEntityInput(iEntity, "Kill");
				}
			}

			// Client can build Sentry
			if (iCount < g_iBuildingLimit[TDBuilding_Sentry]) {
				return true;
			} else {
				Forbid(iClient, true, "%s %t", PLUGIN_PREFIX, "forbidSentryLimit", g_iBuildingLimit[TDBuilding_Sentry]);
				return false;
			}
		}
		case TDBuilding_Dispenser: {
			if (GetClientMetal(iClient) < 100) {
				Forbid(iClient, true, "%s %t", PLUGIN_PREFIX, "forbidInsufficientMetal", 100);
				return false;
			}

			int iEntity = -1, iCount = 0, iOwner = -1;

			while ((iEntity = FindEntityByClassname(iEntity, "obj_dispenser")) != -1) {
				iOwner = GetEntPropEnt(iEntity, Prop_Send, "m_hBuilder");

				if (iOwner == iClient) {
					iCount++;
				}

				if (iCount > g_iBuildingLimit[TDBuilding_Dispenser]) {
					AcceptEntityInput(iEntity, "Kill");
				}
			}

			// Client can build Dispenser
			if (iCount < g_iBuildingLimit[TDBuilding_Dispenser]) {
				Player_CAddValue(iClient, PLAYER_OBJECTS_BUILT, 1);
				return true;
			} else {
				Forbid(iClient, true, "%s %t", PLUGIN_PREFIX, "forbidDispenserLimit", g_iBuildingLimit[TDBuilding_Dispenser]);
				return false;
			}
		}
	}

	return false;
}

/**
 * Prints a colored (default red) forbid message to a client and plays by default a sound.
 *
 * @param iClient 		The client.
 * @param bPlaySound	Play the sound or not.
 * @param sMessage		The message.
 * @param ...			Message formatting parameters.
 * @noreturn
 */

stock void Forbid(int iClient, bool bPlaySound = true, const char[] sMessage, any...) {
	char sFormattedMessage[512];
	VFormat(sFormattedMessage, sizeof(sFormattedMessage), sMessage, 4);

	CPrintToChat(iClient, "%s", sFormattedMessage);

	if (bPlaySound) {
		PlaySound("Forbid", iClient);
	}
}

/**
 * Reloads the current map.
 *
 * @noreturn
 */

stock void ReloadMap() {
	char sCurrentMap[PLATFORM_MAX_PATH];
	GetCurrentMap(sCurrentMap, sizeof(sCurrentMap));

	ServerCommand("changelevel %s", sCurrentMap);
}

/**
 * Removes a flag from a cvar.
 *
 * @param sCvar		The cvar which is affected.
 * @param iFlag		The flag to remove.
 * @noreturn
 */

stock void StripConVarFlag(char[] sCvar, int iFlag) {
	FindConVar(sCvar).Flags &= ~iFlag;
}

/**
 * Adds a flag to a cvar.
 *
 * @param sCvar		The cvar which is affected.
 * @param iFlag		The flag to add.
 * @noreturn
 */

stock void AddConVarFlag(char[] sCvar, int iFlag) {
	FindConVar(sCvar).Flags &= iFlag;
}

/**
 * Checks if a client inside another client.
 *
 * @param iClient		The client to check.
 * @return				True if inside, false otherwise.
 */

stock bool IsInsideClient(int iClient) {
	float fMinBounds[3], fMaxBounds[3], fLocation[3];

	GetClientMins(iClient, fMinBounds);
	GetClientMaxs(iClient, fMaxBounds);

	GetClientAbsOrigin(iClient, fLocation);

	TR_TraceHullFilter(fLocation, fLocation, fMinBounds, fMaxBounds, MASK_SOLID, TraceRayPlayers, iClient);

	return TR_DidHit();
}

/**
 * Shows a custom HUD message to a client.
 *
 * @param iClient		The client.
 * @param sMessage		The message to show.
 * @param ...			Message formatting parameters.
 * @noreturn
 */

stock void PrintToHud(int iClient, const char[] sMessage, any...) {
	if (!IsValidClient(iClient) || !IsClientInGame(iClient)) {
		return;
	}

	char sFormattedMessage[256];
	VFormat(sFormattedMessage, sizeof(sFormattedMessage), sMessage, 3);

	Handle hBuffer = StartMessageOne("KeyHintText", iClient);

	BfWriteByte(hBuffer, 1);
	BfWriteString(hBuffer, sFormattedMessage);
	EndMessage();
}

/**
 * Shows a custom HUD message to all clients.
 *
 * @param sMessage		The message to show.
 * @param ...			Message formatting parameters.
 * @noreturn
 */

stock void PrintToHudAll(const char[] sMessage, any...) {
	char sFormattedMessage[256];

	for (int iClient = 1; iClient <= MaxClients; iClient++) {
		if (IsClientInGame(iClient) && !IsFakeClient(iClient)) {
			VFormat(sFormattedMessage, sizeof(sFormattedMessage), sMessage, 2);
			PrintToHud(iClient, sFormattedMessage);
		}
	}
}

/**
 * Gets a client by name.
 *
 * @param sName			The clients name.
 * @param iTeam			The clients team, -1 for both teams.
 * @return				The client, or -1 on error.
 */

stock int GetClientByNameExact(char[] sName, int iTeam = -1) {
	for (int iClient = 1; iClient <= MaxClients; iClient++) {
		if (IsClientInGame(iClient)) {
			if (iTeam == -1) {
				if (StrEqual(sName, GetClientNameShort(iClient))) {
					return iClient;
				}
			} else if (GetClientTeam(iClient) == iTeam) {
				if (StrEqual(sName, GetClientNameShort(iClient))) {
					return iClient;
				}
			}
		}
	}

	return -1;
}

/**
 * Attaches a annotation to an entity.
 *
 * @param iEntity		The entity.
 * @param fLifetime		The lifetime of the annotation.
 * @param sMessage		The message to show.
 * @param ...			Message formatting parameters.
 * @noreturn
 */

stock void AttachAnnotation(int iEntity, float fLifetime, char[] sMessage, any...) {
	Handle hEvent = CreateEvent("show_annotation");

	if (hEvent == null) {
		return;
	}

	SetEventInt(hEvent, "follow_entindex", iEntity);
	SetEventInt(hEvent, "id", iEntity);
	SetEventFloat(hEvent, "lifetime", fLifetime);
	SetEventString(hEvent, "play_sound", "misc/null.wav");

	char sFormattedMessage[256];
	VFormat(sFormattedMessage, sizeof(sFormattedMessage), sMessage, 4);
	SetEventString(hEvent, "text", sFormattedMessage);

	FireEvent(hEvent);
}

/**
 * Hides the annotation which is attached to an entity.
 *
 * @param iEntity		The entity.
 * @noreturn
 */

stock void HideAnnotation(int iEntity) {
	Handle hEvent = CreateEvent("hide_annotation");

	if (hEvent == null) {
		return;
	}

	SetEventInt(hEvent, "id", iEntity);
	FireEvent(hEvent);
}

/**
 * Attaches a annotation to an entity.
 *
 * @param iClient		The client.
 * @param iEntity		The entity.
 * @param fLifetime		The lifetime of the annotation.
 * @param sMessage		The message to show.
 * @param ...			Message formatting parameters.
 * @noreturn
 */

stock void AttachAdvancedAnnotation(int iClient, int iEntity, float fLifetime, char[] sMessage, any...) {
	Handle hEvent = CreateEvent("show_annotation");

	if (hEvent == null) {
		return;
	}

	SetEventInt(hEvent, "follow_entindex", iEntity);
	SetEventInt(hEvent, "id", iClient * iEntity);
	SetEventFloat(hEvent, "lifetime", fLifetime);
	SetEventString(hEvent, "play_sound", "misc/null.wav");

	char sFormattedMessage[256];
	VFormat(sFormattedMessage, sizeof(sFormattedMessage), sMessage, 5);
	SetEventString(hEvent, "text", sFormattedMessage);

	SetEventInt(hEvent, "visibilityBitfield", GetVisibilityBitfield(iClient));

	FireEvent(hEvent);
}

/**
 * Hides the annotation which is attached to an entity.
 *
 * @param iEntity		The client.
 * @param iEntity		The entity.
 * @noreturn
 */

stock void HideAdvancedAnnotation(int iClient, int iEntity) {
	Handle hEvent = CreateEvent("hide_annotation");

	if (hEvent == null) {
		return;
	}

	SetEventInt(hEvent, "id", iClient * iEntity);
	FireEvent(hEvent);
}

/**
 * Shows an annotation at a given location.
 *
 * @param iId			The id (use this to hide).
 * @param fLocation		The location vector.
 * @param fLifetime		The lifetime of the annotation.
 * @param sMessage		The message to show.
 * @param ...			Message formatting parameters.
 * @noreturn
 */

stock void ShowAnnotation(int iId, float fLocation[3], float fOffsetZ, float fLifetime, char[] sMessage, any...) {
	Handle hEvent = CreateEvent("show_annotation");

	if (hEvent == null) {
		return;
	}

	SetEventFloat(hEvent, "worldPosX", fLocation[0]);
	SetEventFloat(hEvent, "worldPosY", fLocation[1]);
	SetEventFloat(hEvent, "worldPosZ", fLocation[2] + fOffsetZ);
	SetEventInt(hEvent, "id", iId);
	SetEventFloat(hEvent, "lifetime", fLifetime);
	SetEventString(hEvent, "play_sound", "misc/null.wav");

	char sFormattedMessage[256];
	VFormat(sFormattedMessage, sizeof(sFormattedMessage), sMessage, 4);
	SetEventString(hEvent, "text", sFormattedMessage);

	FireEvent(hEvent);
}

/**
 * Converts an integer value to its absolute value.
 *
 * @param iValue		The value.
 * @return				The absolute value.
 */

stock int Abs(int iValue) {
	return (iValue < 0 ? -iValue : iValue);
}

/**
 * Sets the servers password.
 *
 * @param sPassword 	The password to set.
 * @param bDatabase 	Save the password in the database.
 * @param bReloadMap 	Reload map afterwards.
 * @noreturn
 */

stock void SetPassword(const char[] sPassword, bool bDatabase = true, bool bReloadMap = false) {
	ServerCommand("sv_password \"%s\"", sPassword);

	if (bDatabase) {
		Database_SetServerPassword(sPassword, bReloadMap);
	} else {
		Log(TDLogLevel_Debug, "Set server password to \"%s\"", sPassword);
	}
}

/**
 * Initializes a map (trie) handle.
 *
 * @param hMapHandle	The map handle to initialize.
 * @noreturn
 */

stock void CreateDataMap(Handle &hMapHandle) {
	if (hMapHandle != null) {
		CloseHandle(hMapHandle);
		hMapHandle = null;
	}

	hMapHandle = CreateTrie();
}

/**
 * Gets the entity index of the health bar.
 *
 * @return				The entity index of the health bar.
 */

stock int GetHealthBar() {
	int iHealthBar = FindEntityByClassname(-1, "monster_resource");

	if (!IsValidEntity(iHealthBar)) {
		iHealthBar = CreateEntityByName("monster_resource");

		if (IsValidEntity(iHealthBar)) {
			DispatchSpawn(iHealthBar);
		}
	}

	SetEntProp(iHealthBar, Prop_Send, "m_iBossHealthPercentageByte", 0);

	return iHealthBar;
}

stock int GetClosestClient(int iClient) {
	float fClientLocation[3];
	GetClientAbsOrigin(iClient, fClientLocation);
	float fEntityOrigin[3];

	int	  iClosestEntity   = -1;
	float fClosestDistance = -1.0;
	for (int i = 1; i < MaxClients; i++)
		if (IsValidClient(i)) {
			if (IsClientConnected(i) && IsClientInGame(i) && GetClientTeam(i) != GetClientTeam(iClient) && i > 0) {
				GetClientAbsOrigin(i, fEntityOrigin);
				float fEntityDistance = GetVectorDistance(fClientLocation, fEntityOrigin);
				if ((fEntityDistance < fClosestDistance) || fClosestDistance == -1.0) {
					fClosestDistance = fEntityDistance;
					iClosestEntity	 = i;
				}
			}
		}
	return iClosestEntity;
}

stock char[] GetClientNameShort(int iClient) {
	// Steam username is limited to 32 chars, sourcemod says 128?
	char sName[MAX_NAME_LENGTH];
	GetClientName(iClient, sName, sizeof(sName));
	return sName;
}