#pragma semicolon 1


/****** I N C L U D E S *****/

#include <sourcemod>
#include <sdktools>
#include <sdkhooks>
#include <tf2_stocks>
#include <tf2attributes>

#undef REQUIRE_PLUGIN
#include <updater>
#include <friendly>
#include <friendlysimple>

#include "rtd/#perks.sp"


/******* D E F I N E S ******/

#define PLUGIN_VERSION	"1.02"

#define CHAT_PREFIX 	"\x07FFD700[RTD]\x01"
#define CONS_PREFIX 	"[RTD]"

#define PERK_MAX_COUNT	64
#define PERK_MAX_LOW	32
#define PERK_MAX_HIGH	64
#define PERK_MAX_VERYH	128

#define CHAT_AD			1
#define CHAT_APPROLLER	2
#define CHAT_APPOTHER	4
#define CHAT_REMROLLER	8
#define CHAT_REMOTHER	16
#define CHAT_REASONS	32

#define PERK_COLOR_GOOD	"\x0732CD32"
#define PERK_COLOR_BAD	"\x078650AC"

#define FLAG_FEIGNDEATH	(1 << 5)
#define FLAGS_CVARS		FCVAR_PLUGIN|FCVAR_NOTIFY

#define LASERBEAM		"sprites/laserbeam.vmt"

#define UPDATE_URL		"https://bitbucket.org/Phil25/rtd/raw/default/update.txt"



/********* E N U M S ********/

enum Perks{

	String:sName[PERK_MAX_LOW],
	bool:bGood,
	String:sSound[PERK_MAX_HIGH],
	String:sToken[PERK_MAX_LOW],
	iTime,
	iClasses,
	Handle:hWeaponClasses,
	String:sPref[PERK_MAX_HIGH],
	Handle:hTags,
	bool:bIsDisabled,
	
	bool:bIsExternal,
	Function:funcCallback,
	Handle:plParent

};		int ePerks[PERK_MAX_COUNT][Perks];

enum ClientInfo{

	bool:bRolling,
	iLastRoll,
	iLastPerk,
	iGreatLastPerk,
	iCurPerk,
	iPerkEnd,
	Handle:hPerkTimer,
	Handle:hHudSync,
	iGroupRollId

};	int eClients[MAXPLAYERS+1][ClientInfo];

enum GroupRolls{

	bool:bActive,
	iGroupPerkId,
	Handle:hClientArray,
	iClientCount

}	int eGroup[MAXPLAYERS+1][GroupRolls];



/********* M A N A G E R ********/

#include "rtd/#manager.sp" //For info, go to the script itself



/***** V A R I A B L E S ****/

char	g_sTeamColors[][]		= {"\x07B2B2B2", "\x07B2B2B2", "\x07FF4040", "\x0799CCFF"};
int		g_iPerkCount			= 0;
int		g_iCorePerkCount		= 0;
bool	g_bTempPrint			= false;

bool	g_bPluginUpdater		= false;
bool	g_bPluginFriendly		= false;
bool	g_bPluginFriendlySimple	= false;

bool	g_bIsRegisteringOpen	= false;
bool	g_bIsUpdateForced		= false;

Handle	g_hDescriptionMenu		= INVALID_HANDLE;

bool	g_bIsGameArena			= false;



/***** C O N V A R S ****/

Handle g_hCvarPluginEnabled;		bool g_bCvarPluginEnabled = true;
#define DESC_PLUGIN_ENABLED "0/1 - Enable or disable the plugin."
Handle g_hCvarAutoUpdate;			bool g_bCvarAutoUpdate = true;
#define DESC_AUTO_UPDATE "0/1 - Enable or disable automatic updating of the plugin when Updater is installed."
Handle g_hCvarReloadUpdate;			bool g_bCvarReloadUpdate = true;
#define DESC_RELOAD_UPDATE "0/1 - Enable or disable automatic plugin reloading when a new version has been downloaded."
Handle g_hCvarLog;					bool g_bCvarLog = false;
#define DESC_LOG "0/1 - Log RTD actions to SourceMod logs?"
Handle g_hCvarChat;					int g_iCvarChat = 63;
#define DESC_CHAT "Add/substract these values to control messages:\n1 - RTD ad (round start)\n2 - Perk applic. for rollers\n4 - P. applic. for others\n8 - P. removal for rollers\n16 - P. removal for others\n32 - Can't-roll reasons\nEXAMPLE: \"6\" - show applications only (2 + 4)"

Handle g_hCvarPerkDuration;			int g_iCvarPerkDuration = 25;
#define DESC_PERK_DURATION "Default time for the perk to last. This value can be overridden for any perk in rtd2_perks.cfg"
Handle g_hCvarRollInterval;			int g_iCvarRollInterval = 60;
#define DESC_ROLL_INTERVAL "Time in seconds a client has to wait to roll again after a perk has finished."
Handle g_hCvarDisabledPerks;
#define DESC_DISABLED_PERKS "Enter the effects you'd like to disable, seperated by commas. You can use IDs, tokens or tags which occur in a single perk. ('0, toxic, sandvich' disables first 3)"

Handle g_hCvarAllowed;				int g_iCvarAllowed = 0;
#define DESC_ALLOWED "Admin flags which are required to use RTD. If blank, all is assumed."
Handle g_hCvarInSetup;				bool g_bCvarInSetup = true;
#define DESC_IN_SETUP "0/1 - Can RTD be used during Setup?"
Handle g_hCvarTriggers;				Handle g_arrCvarTriggers = INVALID_HANDLE;	int g_iCvarTriggers = 2;
#define DESC_TRIGGERS "Chat triggers which will initiate rolls, seperated by comma."
Handle g_hCvarShowTriggers;			bool g_bCvarShowTriggers = false;
#define DESC_SHOW_TRIGGERS "0/1 - Should the chat triggers be shown once they're typed?"
Handle g_hCvarShowTime;				bool g_bCvarShowTime = false;
#define DESC_SHOW_TIME "0/1 - Should time the perk was applied for be displayed?"

Handle g_hCvarRtdTeam;				int g_iCvarRtdTeam = 0;
#define DESC_RTD_TEAM "0 - both teams can roll, 1 - only RED team can roll, 2 - only BLU team can roll."
Handle g_hCvarRtdMode;				int g_iCvarRtdMode = 0;
#define DESC_RTD_MODE "0 - No restrain except the interval, 1 - Limit by rollers, 2 - Limit by rollers in team."
Handle g_hCvarClientLimit;			int g_iCvarClientLimit = 2;
#define DESC_CLIENT_LIMIT "How many players could use RTD at once. Active only when RTD Mode is 1"
Handle g_hCvarTeamLimit;			int g_iCvarTeamLimit = 2;
#define DESC_TEAM_LIMIT "How many players in each team could use RTD at once. Active only when RTD Mode is 2"
Handle g_hCvarRespawnStuck;			bool g_bCvarRespawnStuck = true;
#define DESC_RESPAWN_STUCK "0/1 - Should a player be forcibly respawned when a perk has ended and he's detected stuck?"

Handle g_hCvarCanRepeatPerk;		bool g_bCvarCanRepeatPerk = false;
#define DESC_CAN_REPEAT_PERK "0/1 - Can a perk can be allowed to be rolled twice in a row."
Handle g_hCvarCanRepeatGreatPerk;	bool g_bCvarCanRepeatGreatPerk = false;
#define DESC_CAN_REPEAT_GREAT_PERK "0/1 - Can a perk can be allowed to be rolled the second time in 3 rolls."

Handle g_hCvarGoodChance;			float g_fCvarGoodChance = 0.5;
#define DESC_GOOD_CHANCE "0.0-1.0 - Chance of rolling a good perk. If there are no good perks available, a bad one will be tried to be rolled instead."
Handle g_hCvarGoodDonatorChance;	float g_fCvarGoodDonatorChance = 0.5;
#define DESC_GOOD_DONATOR_CHANCE "0.0-1.0 - Chance of rolling a good perk if roller is a donator. If there are no good perks available, a bad one will be tried to roll instead."
Handle g_hCvarDonatorFlag;			int g_iCvarDonatorFlag = 0;
#define DESC_DONATOR_FLAG "Admin flag required for donators."

Handle g_hCvarTimerPosX;			float g_fCvarTimerPosX = -1.0;
#define DESC_TIMER_POS_X "0.0-1.0 - The X position of the perk HUD timer display. -1.0 to center."
Handle g_hCvarTimerPosY;			float g_fCvarTimerPosY = 0.55;
#define DESC_TIMER_POS_Y "0.0-1.0 - The Y position of the perk HUD timer display. -1.0 to center."



/***** F O R W A R D S ****/

Handle g_hFwdCanRoll;
Handle g_hFwdCanForce;
Handle g_hFwdCanRemove;
Handle g_hFwdRolled;
Handle g_hFwdRemoved;
Handle g_hFwdOnRegOpen;



/********** I N F O *********/

public Plugin myinfo = {

	name = "Roll The Dice (Revamped)",
	author = "Phil25",
	description = "Lets players roll for temporary benefits.",
	version	= PLUGIN_VERSION,
	url = "https://forums.alliedmods.net/showthread.php?t=278579"

};



			//*************************//
			//----  G E N E R A L  ----//
			//*************************//

public APLRes AskPluginLoad2	(Handle hMyself, bool bLate, char[] sError, int iErrorSize){

	char sGame[32]; sGame[0] = '\0';
	GetGameFolderName(sGame, sizeof(sGame));
	if(!StrEqual(sGame, "tf")){
	
		Format(sError, iErrorSize, "%s This plugin only works for Team Fortress 2.", CONS_PREFIX);
		return APLRes_Failure;
	
	}

	CreateNative("RTD2_GetClientPerkId",	Native_GetClientPerkId);
	CreateNative("RTD2_GetClientPerkTime",	Native_GetClientPerkTime);
	
	CreateNative("RTD2_ForcePerk",			Native_ForcePerk);
	CreateNative("RTD2_RollPerk",			Native_RollPerk);
	CreateNative("RTD2_RemovePerk",			Native_RemovePerk);
	
	CreateNative("RTD2_GetPerkOfString",	Native_GetPerkOfString);
	
	CreateNative("RTD2_RegisterPerk",		Native_RegisterPerk);
	CreateNative("RTD2_IsRegOpen",			Native_IsRegisteringOpen);
	CreateNative("RTD2_SetPerkByToken",		Native_SetPerkByToken);
	CreateNative("RTD2_SetPerkById",		Native_SetPerkById);
	CreateNative("RTD2_DefaultCorePerk",	Native_DefaultCorePerk);
	
	CreateNative("RTD2_CanPlayerBeHurt",	Native_CanPlayerBeHurt);

	RegPluginLibrary("RollTheDice2");

	return APLRes_Success;

}

public void OnPluginStart		(){

	LoadTranslations("rtd2.phrases.txt");
	LoadTranslations("rtd2_perks.phrases.txt");
	LoadTranslations("common.phrases.txt");
	
	if(!ParseEffects())
		return;
	
	ParseCustomEffects();

		//-----[ ConVars ]-----//
	CreateConVar("sm_rtd2_version", PLUGIN_VERSION, "Current RTD2 Version", FLAGS_CVARS|FCVAR_DONTRECORD|FCVAR_SPONLY);

	g_hCvarPluginEnabled		= CreateConVar("sm_rtd2_enabled",		"1",		DESC_PLUGIN_ENABLED,		FLAGS_CVARS);
	g_hCvarAutoUpdate			= CreateConVar("sm_rtd2_autoupdate",	"1",		DESC_AUTO_UPDATE,			FLAGS_CVARS);
	g_hCvarReloadUpdate			= CreateConVar("sm_rtd2_reloadupdate",	"1",		DESC_RELOAD_UPDATE,			FLAGS_CVARS);
	g_hCvarLog					= CreateConVar("sm_rtd2_log",			"0",		DESC_LOG,					FLAGS_CVARS);
	g_hCvarChat					= CreateConVar("sm_rtd2_chat",			"63",		DESC_CHAT,					FLAGS_CVARS);

	g_hCvarPerkDuration			= CreateConVar("sm_rtd2_duration",		"25",		DESC_PERK_DURATION,			FLAGS_CVARS);
	g_hCvarRollInterval			= CreateConVar("sm_rtd2_interval",		"60",		DESC_ROLL_INTERVAL,			FLAGS_CVARS);
	g_hCvarDisabledPerks		= CreateConVar("sm_rtd2_disabled",		"",			DESC_DISABLED_PERKS,		FLAGS_CVARS);

	g_hCvarAllowed				= CreateConVar("sm_rtd2_accessflags",	"",			DESC_ALLOWED,				FLAGS_CVARS);
	g_hCvarInSetup				= CreateConVar("sm_rtd2_insetup",		"0",		DESC_IN_SETUP,				FLAGS_CVARS, true, 0.0, true, 1.0);
	g_hCvarTriggers				= CreateConVar("sm_rtd2_triggers",		"rtd,roll",	DESC_TRIGGERS,				FLAGS_CVARS);
	g_hCvarShowTriggers			= CreateConVar("sm_rtd2_showtriggers",	"0",		DESC_SHOW_TRIGGERS,			FLAGS_CVARS, true, 0.0, true, 1.0);
	g_hCvarShowTime				= CreateConVar("sm_rtd2_showtime",		"0",		DESC_SHOW_TIME,				FLAGS_CVARS, true, 0.0, true, 1.0);

	g_hCvarRtdTeam				= CreateConVar("sm_rtd2_team",			"0",		DESC_RTD_TEAM,				FLAGS_CVARS, true, 0.0, true, 2.0);
	g_hCvarRtdMode				= CreateConVar("sm_rtd2_mode",			"0",		DESC_RTD_MODE,				FLAGS_CVARS, true, 0.0, true, 2.0);
	g_hCvarClientLimit			= CreateConVar("sm_rtd2_playerlimit",	"2",		DESC_CLIENT_LIMIT,			FLAGS_CVARS, true, 0.0);
	g_hCvarTeamLimit			= CreateConVar("sm_rtd2_teamlimit",		"2",		DESC_TEAM_LIMIT,			FLAGS_CVARS, true, 0.0);
	g_hCvarRespawnStuck			= CreateConVar("sm_rtd2_respawnstuck",	"1",		DESC_RESPAWN_STUCK,			FLAGS_CVARS, true, 0.0, true, 1.0);

	g_hCvarCanRepeatPerk		= CreateConVar("sm_rtd2_repeat", 		"0",		DESC_CAN_REPEAT_PERK,		FLAGS_CVARS, true, 0.0, true, 1.0);
	g_hCvarCanRepeatGreatPerk	= CreateConVar("sm_rtd2_repeatgreat",	"0",		DESC_CAN_REPEAT_GREAT_PERK,	FLAGS_CVARS, true, 0.0, true, 1.0);

	g_hCvarGoodChance			= CreateConVar("sm_rtd2_chance",		"0.5",		DESC_GOOD_CHANCE,			FLAGS_CVARS, true, 0.0, true, 1.0);
	g_hCvarGoodDonatorChance	= CreateConVar("sm_rtd2_dchance",		"0.75",		DESC_GOOD_DONATOR_CHANCE,	FLAGS_CVARS, true, 0.0, true, 1.0);
	g_hCvarDonatorFlag			= CreateConVar("sm_rtd2_donatorflag",	"",			DESC_DONATOR_FLAG,			FLAGS_CVARS);

	g_hCvarTimerPosX			= CreateConVar("sm_rtd2_timerpos_x",	"-1.0",		DESC_TIMER_POS_X,			FLAGS_CVARS);
	g_hCvarTimerPosY			= CreateConVar("sm_rtd2_timerpos_y",	"0.55",		DESC_TIMER_POS_Y,			FLAGS_CVARS);


		//-----[ ConVars Hooking & Setting ]-----//
	HookConVarChange(g_hCvarPluginEnabled,		ConVarChange_Plugin	);	g_bCvarPluginEnabled		= GetConVarInt(g_hCvarPluginEnabled) > 0 ? true : false;
	HookConVarChange(g_hCvarAutoUpdate,			ConVarChange_Plugin	);	g_bCvarAutoUpdate			= GetConVarInt(g_hCvarAutoUpdate) > 0 ? true : false;
	HookConVarChange(g_hCvarReloadUpdate,		ConVarChange_Plugin	);	g_bCvarReloadUpdate			= GetConVarInt(g_hCvarReloadUpdate) > 0 ? true : false;
	HookConVarChange(g_hCvarLog,				ConVarChange_Plugin	);	g_bCvarLog					= GetConVarInt(g_hCvarLog) > 0 ? true : false;
	HookConVarChange(g_hCvarChat,				ConVarChange_Plugin	);	g_iCvarChat					= GetConVarInt(g_hCvarChat);

	HookConVarChange(g_hCvarPerkDuration,		ConVarChange_Perks	);	g_iCvarPerkDuration			= GetConVarInt(g_hCvarPerkDuration);
	HookConVarChange(g_hCvarRollInterval,		ConVarChange_Perks	);	g_iCvarRollInterval			= GetConVarInt(g_hCvarRollInterval);
	HookConVarChange(g_hCvarDisabledPerks,		ConVarChange_Perks	);

	HookConVarChange(g_hCvarAllowed,			ConVarChange_Usage	);	g_iCvarAllowed				= ReadFlagFromConVar(g_hCvarAllowed);
	HookConVarChange(g_hCvarInSetup,			ConVarChange_Usage	);	g_bCvarInSetup				= GetConVarInt(g_hCvarInSetup) > 0 ? true : false;
	HookConVarChange(g_hCvarTriggers,			ConVarChange_Usage	);	ParseTriggers();
	HookConVarChange(g_hCvarShowTriggers,		ConVarChange_Usage	);	g_bCvarShowTriggers			= GetConVarInt(g_hCvarShowTriggers) > 0 ? true : false;
	HookConVarChange(g_hCvarShowTime,			ConVarChange_Usage	);	g_bCvarShowTime				= GetConVarInt(g_hCvarShowTime) > 0 ? true : false;

	HookConVarChange(g_hCvarRtdTeam,			ConVarChange_Rtd	);	g_iCvarRtdTeam				= GetConVarInt(g_hCvarRtdTeam);
	HookConVarChange(g_hCvarRtdMode,			ConVarChange_Rtd	);	g_iCvarRtdMode				= GetConVarInt(g_hCvarRtdMode);
	HookConVarChange(g_hCvarClientLimit,		ConVarChange_Rtd	);	g_iCvarClientLimit			= GetConVarInt(g_hCvarClientLimit);
	HookConVarChange(g_hCvarTeamLimit,			ConVarChange_Rtd	);	g_iCvarTeamLimit			= GetConVarInt(g_hCvarTeamLimit);
	HookConVarChange(g_hCvarRespawnStuck,		ConVarChange_Rtd	);	g_bCvarRespawnStuck			= GetConVarInt(g_hCvarRespawnStuck) > 0 ? true : false;

	HookConVarChange(g_hCvarCanRepeatPerk,		ConVarChange_Repeat	);	g_bCvarCanRepeatPerk		= GetConVarInt(g_hCvarCanRepeatPerk) > 0 ? true : false;
	HookConVarChange(g_hCvarCanRepeatGreatPerk,	ConVarChange_Repeat	);	g_bCvarCanRepeatGreatPerk	= GetConVarInt(g_hCvarCanRepeatGreatPerk) > 0 ? true : false;

	HookConVarChange(g_hCvarGoodChance,			ConVarChange_Good	);	g_fCvarGoodChance			= GetConVarFloat(g_hCvarGoodChance);
	HookConVarChange(g_hCvarGoodDonatorChance,	ConVarChange_Good	);	g_fCvarGoodDonatorChance	= GetConVarFloat(g_hCvarGoodDonatorChance);
	HookConVarChange(g_hCvarDonatorFlag,		ConVarChange_Good	);	g_iCvarDonatorFlag			= ReadFlagFromConVar(g_hCvarDonatorFlag);

	HookConVarChange(g_hCvarTimerPosX,			ConVarChange_Timer	);	g_fCvarTimerPosX			= GetConVarFloat(g_hCvarTimerPosX);
	HookConVarChange(g_hCvarTimerPosY,			ConVarChange_Timer	);	g_fCvarTimerPosY			= GetConVarFloat(g_hCvarTimerPosY);

	AutoExecConfig(true);


		//-----[ Forwards ]-----//
	g_hFwdCanRoll	= CreateGlobalForward("RTD2_CanRollDice",	ET_Event, Param_Cell						);
	g_hFwdCanForce	= CreateGlobalForward("RTD2_CanForcePerk",	ET_Event, Param_Cell, Param_Cell, Param_Cell);
	g_hFwdCanRemove	= CreateGlobalForward("RTD2_CanRemovePerk",	ET_Event, Param_Cell, Param_Cell, Param_Cell);
	g_hFwdRolled	= CreateGlobalForward("RTD2_Rolled",		ET_Event, Param_Cell, Param_Cell, Param_Cell);
	g_hFwdRemoved	= CreateGlobalForward("RTD2_Removed",		ET_Event, Param_Cell, Param_Cell, Param_Cell);
	g_hFwdOnRegOpen	= CreateGlobalForward("RTD2_OnRegOpen",		ET_Ignore									);


		//-----[ Commands ]-----//
	RegAdminCmd("sm_rtd",		Command_RTD,			0,				"Roll a perk.");
	RegAdminCmd("sm_perks",		Command_DescMenu,		0,				"Display a description menu of RTD perks.");

	RegAdminCmd("sm_forcertd",	Command_ForceRTD,		ADMFLAG_SLAY,	"Applies perk to selected player(s).");
	RegAdminCmd("sm_removertd",	Command_RemoveRTD,		ADMFLAG_SLAY,	"Removes perk from selected player(s).");

	RegAdminCmd("sm_rtds",		Command_PerkSearchup,	ADMFLAG_SLAY,	"Displays customized perk list.");
	RegAdminCmd("sm_rtdsearch",	Command_PerkSearchup,	ADMFLAG_SLAY,	"Displays customized perk list.");

	RegAdminCmd("sm_reloadrtd",	Command_Reload,			ADMFLAG_CONFIG,	"Reloads the config files.");
	RegAdminCmd("sm_updatertd",	Command_Update,			ADMFLAG_ROOT,	"Force an update check. Does nothing if Updater is not installed.");


		//-----[ Listeners ]-----//
	AddCommandListener(Listener_Say,	"say");
	AddCommandListener(Listener_Say,	"say_team");
	AddCommandListener(Listener_Voice,	"voicemenu");

	for(int i = 1; i <= MaxClients; i++)
		if(IsValidClient(i))
			OnClientPutInServer(i);

}

public void OnConfigsExecuted	(){

	g_hDescriptionMenu		= BuildDesc();
	g_bIsRegisteringOpen	= true;
	
	Forward_OnRegOpen();
	ParseDisabledPerks();

}

public void OnPluginEnd			(){

	for(int i = 1; i <= MaxClients; i++){

		Forward_OnRemovePerkPre(i);

		if(eClients[i][bRolling])
			ForceRemovePerk(i, 0);

		eClients[i][bRolling]	= false;
		eClients[i][iLastRoll]	= 0;
		eClients[i][iCurPerk]	= -1;

	}

}

public void OnMapStart			(){

	HookEvent("player_death",			Event_PlayerDeath);
	HookEvent("player_changeclass",		Event_ClassChange);
	HookEvent("teamplay_round_active",	Event_RoundActive);

	PrecacheModel(LASERBEAM);

	Forward_OnMapStart();
	PrecachePerkSounds();
	
	g_bIsGameArena = (FindEntityByClassname(MaxClients +1, "tf_logic_arena") > MaxClients);

}

public void OnMapEnd			(){

	UnhookEvent("player_death",				Event_PlayerDeath);
	UnhookEvent("player_changeclass",		Event_ClassChange);
	UnhookEvent("teamplay_round_active",	Event_RoundActive);

}

public void OnClientPutInServer	(int client){

	eClients[client][bRolling]	= false;
	eClients[client][iLastRoll]	= 0;
	eClients[client][iCurPerk]	= -1;
	eClients[client][hHudSync]	= CreateHudSynchronizer();

	Forward_OnClientPutInServer(client);

}

public void OnClientDisconnect	(int client){

	Forward_OnRemovePerkPre(client);

	if(eClients[client][bRolling])
		ForceRemovePerk(client, 4);

	eClients[client][bRolling]	= false;
	eClients[client][iLastRoll]	= 0;
	eClients[client][iCurPerk]	= -1;

}

public void OnAllPluginsLoaded	(){
	
	g_bPluginUpdater		= LibraryExists("updater");
	g_bPluginFriendly		= LibraryExists("[TF2] Friendly Mode");
	g_bPluginFriendlySimple	= LibraryExists("Friendly Simple");

	if(g_bPluginUpdater)
		Updater_AddPlugin(UPDATE_URL);

}

public void OnLibraryAdded		(const char[] sLibName){

	if(StrEqual(sLibName, "updater"))
		g_bPluginUpdater = true;

	if(StrEqual(sLibName, "[TF2] Friendly Mode"))
		g_bPluginFriendly = true;

	if(StrEqual(sLibName, "Friendly Simple"))
		g_bPluginFriendlySimple = true;

	if(g_bPluginUpdater)
		Updater_AddPlugin(UPDATE_URL);

}

public void OnLibraryRemoved	(const char[] sLibName){

	if(StrEqual(sLibName, "updater"))
		g_bPluginUpdater = false;

	if(StrEqual(sLibName, "[TF2] Friendly Mode"))
		g_bPluginFriendly = false;

	if(StrEqual(sLibName, "Friendly Simple"))
		g_bPluginFriendlySimple = false;

}



			//*************************//
			//----  U P D A T E R  ----//
			//*************************//

public Action Updater_OnPluginChecking	(){

	if(!g_bCvarAutoUpdate && !g_bIsUpdateForced)
		return Plugin_Handled;
	
	g_bIsUpdateForced = false;
	return Plugin_Continue;

}

public int Updater_OnPluginUpdated		(){

	if(g_bCvarReloadUpdate)
		ReloadPlugin();

}



			//***************************//
			//----  C O M M A N D S  ----//
			//***************************//

public Action Command_RTD			(int client, int args){

	if(IsValidClient(client))
		RollPerkForClient(client);

	return Plugin_Handled;

}

public Action Command_DescMenu		(int client, int args){

	if(IsValidClient(client))
		ShowDesc(client);

	return Plugin_Handled;

}

public Action Command_ForceRTD		(int client, int args){

	if(args < 1){

		ReplyToCommand(client, "[SM] Usage: sm_forcertd <player> <perk id>* <time>* <override class restriction (0 / 1)>*");
		return Plugin_Handled;

	}

	char sTrgName[MAX_TARGET_LENGTH], sTrg[32];
	int	 aTrgList[MAXPLAYERS], iTrgCount;
	bool bNameMultiLang;
	GetCmdArg(1, sTrg, sizeof(sTrg));

	if((iTrgCount = ProcessTargetString(sTrg, client, aTrgList, MAXPLAYERS, COMMAND_FILTER_ALIVE, sTrgName, sizeof(sTrgName), bNameMultiLang)) <= 0){

		ReplyToTargetError(client, iTrgCount);
		return Plugin_Handled;

	}

	int		iPerkTime		= -1;
	bool	bOverrideClass	= false;
	char	sPerkString[16]	= "-";

	if(args > 1){

		GetCmdArg(2, sPerkString, sizeof(sPerkString));

		if(args > 2){

			char sPerkTime[8];
			GetCmdArg(3, sPerkTime, sizeof(sPerkTime));
			iPerkTime = StringToInt(sPerkTime);

			if(args > 3){

				char sOverrideClass[2];
				GetCmdArg(4, sOverrideClass, sizeof(sOverrideClass));
				if(StringToInt(sOverrideClass) > 0)
					bOverrideClass = true;

			}

		}

	}

	int iGroup = -1;
	if(iTrgCount > 1){

		iGroup = GetNextAvailableGroup();

		if(iGroup < 0)
			return Plugin_Handled;

		eGroup[iGroup][bActive]			= true;
		eGroup[iGroup][iClientCount]	= iTrgCount;

		if(eGroup[iGroup][hClientArray] == INVALID_HANDLE)
			eGroup[iGroup][hClientArray] = CreateArray();

		ClearArray(eGroup[iGroup][hClientArray]);

	}
	
	for(int i = 0; i < iTrgCount; i++){
	
		if(iGroup > -1)
			PushArrayCell(eGroup[iGroup][hClientArray], GetClientSerial(aTrgList[i]));

		eClients[aTrgList[i]][iGroupRollId] = iGroup;
		ForcePerk(aTrgList[i], sPerkString, 16, iPerkTime, bOverrideClass, iGroup, client);

	}

	return Plugin_Handled;

}

public Action Command_RemoveRTD		(int client, int args){

	if(args < 1){

		ReplyToCommand(client, "[SM] Usage: sm_removertd <player> <\"reason\">*");
		return Plugin_Handled;

	}

	char sTrgName[MAX_TARGET_LENGTH], sTrg[32];
	int	 aTrgList[MAXPLAYERS], iTrgCount;
	bool bNameMultiLang;
	GetCmdArg(1, sTrg, sizeof(sTrg));

	if((iTrgCount = ProcessTargetString(sTrg, client, aTrgList, MAXPLAYERS, COMMAND_FILTER_ALIVE, sTrgName, sizeof(sTrgName), bNameMultiLang)) <= 0){

		ReplyToTargetError(client, iTrgCount);
		return Plugin_Handled;

	}

	char sReason[128] = "";
	if(args > 1)
		GetCmdArg(2, sReason, sizeof(sReason));

	bool bFuncCount = (GetForwardFunctionCount(g_hFwdCanRemove) > 0);
	for(int i = 0; i < iTrgCount; i++){
	
		if(eClients[i][bRolling])
			if(bFuncCount){
	
				Call_StartForward(g_hFwdCanRemove);
				Call_PushCell(client);
				Call_PushCell(aTrgList[i]);
				Call_PushCell(eClients[aTrgList[i]][iCurPerk]);
				Action result = Plugin_Continue;
				Call_Finish(result);
			
				if(result != Plugin_Continue)
					continue;
		
			}

		Forward_OnRemovePerkPre(client);

		if(eClients[aTrgList[i]][bRolling])
			ForceRemovePerk(aTrgList[i], args > 1 ? 5 : 3, sReason);

	}

	return Plugin_Handled;

}

public Action Command_PerkSearchup	(int client, int args){

	if(args < 1){

		for(int i = 0; i < g_iPerkCount; i++){

			PrintToConsole(client, "%d. %s", i, ePerks[i][sName]);

		}

		if(client > 0)
			PrintToChat(client, "%s Perk list has been printed to your console.", CHAT_PREFIX);

		return Plugin_Handled;

	}

	char sTagString[64], sTagBuffer[32];
	for(int i = 1; i <= args; i++){

		GetCmdArg(i, sTagBuffer, sizeof(sTagBuffer));

		if(i < 2){

			Format(sTagString, sizeof(sTagString), "%s", sTagBuffer);
			continue;

		}

		Format(sTagString, sizeof(sTagString), "%s|%s", sTagString, sTagBuffer);

	}

	int iPerksFound = 0;
	for(int j = 0; j < g_iPerkCount; j++){

		if(!IsPerkInTags(j, sTagString, args))
			continue;

		PrintToConsole(client, "%d. %s", j, ePerks[j][sName]);
		iPerksFound++;

	}

	if(client > 0)
	PrintToChat(client, "%s %d %s found matching given criteria.", CHAT_PREFIX, iPerksFound, iPerksFound != 1 ? "perks" : "perk");

	return Plugin_Handled;

}

public Action Command_Reload		(int client, int args){

	ParseEffects();
	ParseCustomEffects();

	return Plugin_Handled;

}

public Action Command_Update		(int client, int args){

	if(!g_bPluginUpdater){
	
		ReplyToCommand(client, "%s Updater is not installed.", CONS_PREFIX);
		return Plugin_Handled;
	
	}

	g_bIsUpdateForced = true;
	
	if(Updater_ForceUpdate())
		ReplyToCommand(client, "%s New RTD version available!", CONS_PREFIX);
	
	else
		ReplyToCommand(client, "%s This RTD version is up to date or unable to update.", CONS_PREFIX);
	
	g_bIsUpdateForced = false;

	return Plugin_Handled;

}



			//*****************************//
			//----  L I S T E N E R S  ----//
			//*****************************//

public Action Listener_Say		(int client, const char[] sCommand, int args){

	if(!IsValidClient(client))
		return Plugin_Continue;

	char sText[16];
	GetCmdArg(1, sText, sizeof(sText));

	if(!IsArgumentTrigger(sText))
		return Plugin_Continue;

	RollPerkForClient(client);

	return g_bCvarShowTriggers ? Plugin_Continue : Plugin_Stop;

}

public Action Listener_Voice	(int client, const char[] sCommand, int args){

	if(IsValidClient(client))
		Forward_Voice(client);

	return Plugin_Continue;

}



			//***********************//
			//----  C O N V A R  ----//
			//***********************//

public int ConVarChange_Plugin	(Handle hCvar, const char[] sOld, const char[] sNew){

	if(hCvar == g_hCvarPluginEnabled)
		g_bCvarPluginEnabled = StringToInt(sNew) > 0 ? true : false;

	else if(hCvar == g_hCvarAutoUpdate)
		g_bCvarAutoUpdate = StringToInt(sNew) > 0 ? true : false;

	else if(hCvar == g_hCvarReloadUpdate)
		g_bCvarReloadUpdate = StringToInt(sNew) > 0 ? true : false;

	else if(hCvar == g_hCvarLog)
		g_bCvarLog = StringToInt(sNew) > 0 ? true : false;

	else if(hCvar == g_hCvarChat)
		g_iCvarChat = StringToInt(sNew);

}

public int ConVarChange_Perks	(Handle hCvar, const char[] sOld, const char[] sNew){

	if(hCvar == g_hCvarPerkDuration)
		g_iCvarPerkDuration = StringToInt(sNew);

	else if(hCvar == g_hCvarRollInterval)
		g_iCvarRollInterval = StringToInt(sNew);

	else if(hCvar == g_hCvarDisabledPerks)
		ParseDisabledPerks();

}

public int ConVarChange_Usage	(Handle hCvar, const char[] sOld, const char[] sNew){

	if(hCvar == g_hCvarAllowed)
		g_iCvarAllowed = ReadFlagString(sNew);

	else if(hCvar == g_hCvarInSetup)
		g_bCvarInSetup = StringToInt(sNew) > 0 ? true : false;

	else if(hCvar == g_hCvarTriggers)
		ParseTriggers();

	else if(hCvar == g_hCvarShowTriggers)
		g_bCvarShowTriggers = StringToInt(sNew) > 0 ? true : false;

	else if(hCvar == g_hCvarShowTime)
		g_bCvarShowTime = StringToInt(sNew) > 0 ? true : false;

}

public int ConVarChange_Rtd		(Handle hCvar, const char[] sOld, const char[] sNew){

	if(hCvar == g_hCvarRtdTeam)
		g_iCvarRtdTeam = StringToInt(sNew);

	else if(hCvar == g_hCvarRtdMode)
		g_iCvarRtdMode = StringToInt(sNew);

	else if(hCvar == g_hCvarClientLimit)
		g_iCvarClientLimit = StringToInt(sNew);

	else if(hCvar == g_hCvarTeamLimit)
		g_iCvarTeamLimit = StringToInt(sNew);

	else if(hCvar == g_hCvarRespawnStuck)
		g_bCvarRespawnStuck = StringToInt(sNew) > 0 ? true : false;

}

public int ConVarChange_Repeat	(Handle hCvar, const char[] sOld, const char[] sNew){

	if(hCvar == g_hCvarCanRepeatPerk)
		g_bCvarCanRepeatPerk = StringToInt(sNew) > 0 ? true : false;

	else if(hCvar == g_hCvarCanRepeatGreatPerk)
		g_bCvarCanRepeatGreatPerk = StringToInt(sNew) > 0 ? true : false;

}

public int ConVarChange_Good	(Handle hCvar, const char[] sOld, const char[] sNew){

	if(hCvar == g_hCvarGoodChance)
		g_fCvarGoodChance = StringToFloat(sNew);

	else if(hCvar == g_hCvarGoodDonatorChance)
		g_fCvarGoodDonatorChance = StringToFloat(sNew);

	else if(hCvar == g_hCvarDonatorFlag)
		g_iCvarDonatorFlag = ReadFlagString(sNew);

}

public int ConVarChange_Timer	(Handle hCvar, const char[] sOld, const char[] sNew){

	if(hCvar == g_hCvarTimerPosX)
		g_fCvarTimerPosX = StringToFloat(sNew);

	else if(hCvar == g_hCvarTimerPosY)
		g_fCvarTimerPosY = StringToFloat(sNew);

}



			//***********************//
			//----  E V E N T S  ----//
			//***********************//

public Action Event_PlayerDeath			(Handle hEvent, const char[] sEventName, bool dontBroadcast){

	int client = GetClientOfUserId(GetEventInt(hEvent, "userid"));
	if(!IsValidClient(client))
		return Plugin_Continue;

	int flags = GetEventInt(hEvent, "death_flags");
	if(flags & FLAG_FEIGNDEATH)
		return Plugin_Continue;

	Forward_OnRemovePerkPre(client);

	if(!eClients[client][bRolling])
		return Plugin_Continue;

	ForceRemovePerk(client, 1);

	return Plugin_Continue;

}

public Action Event_ClassChange			(Handle hEvent, const char[] sEventName, bool dontBroadcast){

	int client = GetClientOfUserId(GetEventInt(hEvent, "userid"));
	if(!IsValidClient(client))		return Plugin_Continue;

	Forward_OnRemovePerkPre(client);

	if(!eClients[client][bRolling])	return Plugin_Continue;

	ForceRemovePerk(client, 2);

	return Plugin_Continue;

}

public Action Event_RoundActive			(Handle hEvent, const char[] sEventName, bool dontBroadcast){

	if(g_bCvarPluginEnabled && (g_iCvarChat & CHAT_AD) && IsRTDInRound())
		PrintToChatAll("%s %T", CHAT_PREFIX, "RTD2_Ad", LANG_SERVER, 0x03, 0x01);
	
	return Plugin_Continue;

}

public void OnEntityCreated				(int iEnt, const char[] sClassname){

	Forward_OnEntityCreated(iEnt, sClassname);

}

public void OnGameFrame					(){

	Forward_OnGameFrame();

}

public void TF2_OnConditionAdded		(int client, TFCond condition){

	Forward_OnConditionAdded(client, condition);

}

public void TF2_OnConditionRemoved		(int client, TFCond condition){

	Forward_OnConditionRemoved(client, condition);

}

public Action OnPlayerRunCmd			(int client, int &iButtons, int &iImpulse, float fVel[3], float fAng[3], int &iWeapon){

	if(!IsValidClient(client))
		return Plugin_Continue;

	if(Forward_OnPlayerRunCmd(client, iButtons, iImpulse, fVel, fAng, iWeapon))
		return Plugin_Changed;
	
	return Plugin_Continue;

}

public Action TF2_CalcIsAttackCritical	(int client, int iWeapon, char[] sWeaponName, bool &bResult){

	if(!IsValidClient(client))
		return Plugin_Continue;

	if(!Forward_AttackIsCritical(client, iWeapon, sWeaponName))
		return Plugin_Continue;

	bResult = true;
	return Plugin_Changed;

}



			//*********************//
			//----  P E R K S  ----//
			//*********************//

//-----[ Parsing & Precaching ]-----//

void ParseTriggers		(){

	char sCvar[64], sBuffer[64];
	GetConVarString(g_hCvarTriggers, sCvar, sizeof(sCvar));
	EscapeString(sCvar, ' ', '\0', sBuffer, sizeof(sBuffer));

	g_iCvarTriggers	= CountCharInString(sBuffer, ',')+1;
	char[][] sPieces	= new char[g_iCvarTriggers][64];

	ExplodeString(sBuffer, ",", sPieces, g_iCvarTriggers, 64);

	if(g_arrCvarTriggers == INVALID_HANDLE)
		g_arrCvarTriggers = CreateArray(16);
	else
		ClearArray(g_arrCvarTriggers);

	for(int i = 0; i < g_iCvarTriggers; i++)
		PushArrayString(g_arrCvarTriggers, sPieces[i]);

}

bool ParseEffects		(){

	char sPath[255];
	BuildPath(Path_SM, sPath, sizeof(sPath), "configs/rtd2_perks.default.cfg");
	
	if(!FileExists(sPath)){
	
		LogError("%s Failed to find rtd2_perks.default.cfg in configs/ folder!", CONS_PREFIX);
		SetFailState("Failed to find rtd2_perks.default.cfg in configs/ folder!");
		return false;
	
	}
	
	g_iPerkCount = 0;
	Handle hKv = CreateKeyValues("Effects");
	int iPrevId = -1, iGood = 0, iBad = 0, iWarnings = 0;
	
	if(FileToKeyValues(hKv, sPath) && KvGotoFirstSubKey(hKv)){
	
		char sPerkId[4], sTokenBuffer[PERK_MAX_LOW], sClassBuffer[2][PERK_MAX_LOW], sWeaponBuffer[2][PERK_MAX_HIGH], sSettingBuffer[PERK_MAX_HIGH], sTagBuffer[2][PERK_MAX_VERYH];
		int iPerkId = 0, iRepeat = -1, iClassFlags = 0, iTagSize = 0;
		do{
	
			KvGetSectionName(hKv, sPerkId, sizeof(sPerkId));
			iPerkId = StringToInt(sPerkId);
			
			if(iPrevId +1 != iPerkId){
			
				LogError("%s Disorder in rtd2_perks.default.cfg detected; aborting! (iPerkId: %d)", CONS_PREFIX, iPerkId);
				SetFailState("Disorder in rtd2_perks.default.cfg detected; aborting! (iPerkId: %d)", iPerkId);
				return false;
			
			}
			iPrevId++;
			
				//----[ NAME ]----//
			
			KvGetString(hKv, "name", ePerks[iPerkId][sName], PERK_MAX_LOW);
			
			
				//----[ GOOD ]----//
			
			ePerks[iPerkId][bGood] = KvGetNum(hKv, "good") > 0 ? true : false;
			
			
				//----[ SOUND ]----//
			
			KvGetString(hKv, "sound", ePerks[iPerkId][sSound], PERK_MAX_HIGH);
			
			
				//----[ TOKEN ]----//
			
			strcopy(sTokenBuffer, PERK_MAX_LOW, "");
			KvGetString(hKv, "token", sTokenBuffer, PERK_MAX_LOW);
			
			EscapeString(sTokenBuffer, ' ', '\0', ePerks[iPerkId][sToken], PERK_MAX_LOW);
			
			iRepeat = FindPerkByToken(ePerks[iPerkId][sToken]);
			if(iRepeat != -1){
			
				LogError("%s Perk \"%s\" (ID:%d) and \"%s\" (ID:%d) have the same token which is forbidden.", CONS_PREFIX, ePerks[iPerkId][sName], iPerkId, ePerks[iRepeat][sName], iRepeat);
				SetFailState("Perk \"%s\" (ID:%d) and \"%s\" (ID:%d) have the same token which is forbidden.", ePerks[iPerkId][sName], iPerkId, ePerks[iRepeat][sName], iRepeat);
				return false;
			
			}
			
			
				//----[ TIME ]----//
			
			ePerks[iPerkId][iTime] = KvGetNum(hKv, "time");
			
			
				//----[ CLASS ]----//
			
			strcopy(sClassBuffer[1], PERK_MAX_HIGH, "");
			KvGetString(hKv, "class", sClassBuffer[0], PERK_MAX_LOW);
			EscapeString(sClassBuffer[0], ' ', '\0', sClassBuffer[1], PERK_MAX_LOW);
			
			iClassFlags = ClassStringToFlags(sClassBuffer[1]);
			if(iClassFlags < 1){

				PrintToServer("%s WARNING: Invalid class restriction(s) set at perk ID:%d (rtd2_perks.default.cfg). Assuming it's all-class.", CONS_PREFIX, iPerkId);
				LogError("%s WARNING: Invalid class restriction(s) set at perk ID:%d (rtd2_perks.default.cfg). Assuming it's all-class.", CONS_PREFIX, iPerkId);
				iWarnings++;
				iClassFlags = 511;

			}ePerks[iPerkId][iClasses] = iClassFlags;
			
			
				//----[ WEAPONS ]----//
			
			strcopy(sWeaponBuffer[1], PERK_MAX_HIGH, "");
			KvGetString(hKv, "weapons", sWeaponBuffer[0], PERK_MAX_HIGH);
			EscapeString(sWeaponBuffer[0], ' ', '\0', sWeaponBuffer[1], PERK_MAX_HIGH);
			
			if(ePerks[iPerkId][hWeaponClasses] == INVALID_HANDLE)
				ePerks[iPerkId][hWeaponClasses] = CreateArray(32);
			else
				ClearArray(ePerks[iPerkId][hWeaponClasses]);
			
			if(FindCharInString(sWeaponBuffer[1], '0') < 0){
			
				int iSize = CountCharInString(sWeaponBuffer[1], ',')+1;
				char[][] sPieces = new char[iSize][32];
				
				ExplodeString(sWeaponBuffer[1], ",", sPieces, iSize, 64);

				for(int i = 0; i < iSize; i++)
					PushArrayString(ePerks[iPerkId][hWeaponClasses], sPieces[i]);
			
			}
			
			
				//----[ SETTINGS ]----//
			
			KvGetString(hKv, "settings", sSettingBuffer, PERK_MAX_HIGH);
			EscapeString(sSettingBuffer, ' ', '\0', ePerks[iPerkId][sPref], PERK_MAX_HIGH);
			
			
				//----[ TAGS ]----//
			
			strcopy(sTagBuffer[1], PERK_MAX_VERYH, ""); iTagSize = 0;
			KvGetString(hKv, "tags", sTagBuffer[0], PERK_MAX_VERYH);
			EscapeString(sTagBuffer[0], ' ', '\0', sTagBuffer[1], PERK_MAX_VERYH);

			if(ePerks[iPerkId][hTags] == INVALID_HANDLE)
				ePerks[iPerkId][hTags] = CreateArray(32);
			else
				ClearArray(ePerks[iPerkId][hTags]);

			if(strlen(sTagBuffer[1]) > 0){

				iTagSize = CountCharInString(sTagBuffer[1], '|')+1;
				char[][] sPieces = new char[iTagSize][24];

				ExplodeString(sTagBuffer[1], "|", sPieces, iTagSize, 24);

				for(int i = 0; i < iTagSize; i++)
					PushArrayString(ePerks[iPerkId][hTags], sPieces[i]);

			}
			
				//----[ STATS ]----//
			
			if(ePerks[iPerkId][bGood])
				iGood++;
			else
				iBad++;
			
			g_iPerkCount++;
			g_iCorePerkCount++;
		
		
		}while(KvGotoNextKey(hKv));
		
		PrintToServer("%s Loaded %d %s (%d good, %d bad; with %d warnings).", CONS_PREFIX, g_iPerkCount, g_iPerkCount > 1 ? "perks" : "perk", iGood, iBad, iWarnings);
	
		if(hKv != INVALID_HANDLE)
			CloseHandle(hKv);
		
		return true;
	
	}
	
	if(hKv != INVALID_HANDLE)
		CloseHandle(hKv);
	
	return false;

}

void ParseCustomEffects	(){

	char sPath[255];
	BuildPath(Path_SM, sPath, sizeof(sPath), "configs/rtd2_perks.custom.cfg");

	if(!FileExists(sPath)){

		PrecachePerkSounds();
		return;

	}

	int iCustomPerkCount = 0;
	Handle hKv = CreateKeyValues("Effects");
	
	if(FileToKeyValues(hKv, sPath) && KvGotoFirstSubKey(hKv)){
	
		Handle hCustomized = CreateArray();
		char sPerkId[4], sClassBuffer[2][PERK_MAX_LOW], sWeaponBuffer[2][PERK_MAX_HIGH], sSettingBuffer[PERK_MAX_HIGH], sTagBuffer[2][PERK_MAX_HIGH];
		int iPerkId = 0, iClassFlags = 0, iTagSize = 0;
		do{

			KvGetSectionName(hKv, sPerkId, sizeof(sPerkId));
			iPerkId = StringToInt(sPerkId);

			if(iPerkId >= g_iPerkCount || iPerkId < 0)
				continue;
			
			
				//----[ NAME ]----//
			
			if(KvJumpToKey(hKv, "name")){
			
				KvGoBack(hKv);
				KvGetString(hKv, "name", ePerks[iPerkId][sName], PERK_MAX_LOW);
			
			}
			
			
				//----[ GOOD ]----//
			
			if(KvJumpToKey(hKv, "good")){
			
				KvGoBack(hKv);
				ePerks[iPerkId][bGood] = KvGetNum(hKv, "good") > 0 ? true : false;
			
			}
			
			
				//----[ SOUND ]----//
			
			if(KvJumpToKey(hKv, "sound")){
			
				KvGoBack(hKv);
				KvGetString(hKv, "sound", ePerks[iPerkId][sSound], PERK_MAX_HIGH);
			
			}
			
			
				//----[ TOKEN ]----//
			
			if(KvJumpToKey(hKv, "token")){
			
				KvGoBack(hKv);
				KvGetString(hKv, "token", ePerks[iPerkId][sToken], PERK_MAX_LOW);
			
			}
			
			
				//----[ TIME ]----//
			
			if(KvJumpToKey(hKv, "time")){
			
				KvGoBack(hKv);
				ePerks[iPerkId][iTime] = KvGetNum(hKv, "time");
			
			}
			
			
				//----[ CLASS ]----//
			
			if(KvJumpToKey(hKv, "class")){
				
				KvGoBack(hKv);
			
				strcopy(sClassBuffer[1], PERK_MAX_LOW, "");
				KvGetString(hKv, "class", sClassBuffer[0], PERK_MAX_LOW, "0");
				EscapeString(sClassBuffer[0], ' ', '\0', sClassBuffer[1], PERK_MAX_LOW);
				
				iClassFlags = ClassStringToFlags(sClassBuffer[1]);
				if(iClassFlags < 1){
	
					PrintToServer("%s WARNING: Invalid class restriction(s) set at perk ID:%d (rtd2_perks.custom.cfg). Assuming it's all-class. (\"%s\")", CONS_PREFIX, iPerkId, sClassBuffer[1]);
					LogError("%s WARNING: Invalid class restriction(s) set at perk ID:%d (rtd2_perks.custom.cfg). Assuming it's all-class. (\"%s\")", CONS_PREFIX, iPerkId, sClassBuffer[1]);
					iClassFlags = 511;
	
				}ePerks[iPerkId][iClasses] = iClassFlags;
			
			}
			
				//----[ WEAPONS ]----//
			
			if(KvJumpToKey(hKv, "weapons")){
				
				KvGoBack(hKv);
			
				strcopy(sWeaponBuffer[1], PERK_MAX_HIGH, "");
				KvGetString(hKv, "weapons", sWeaponBuffer[0], PERK_MAX_HIGH);
				EscapeString(sWeaponBuffer[0], ' ', '\0', sWeaponBuffer[1], PERK_MAX_HIGH);
				
				ClearArray(ePerks[iPerkId][hWeaponClasses]);
				
				if(FindCharInString(sWeaponBuffer[1], '0') < 0){
				
					int iSize = CountCharInString(sWeaponBuffer[1], ',')+1;
					char[][] sPieces = new char[iSize][32];
					
					ExplodeString(sWeaponBuffer[1], ",", sPieces, iSize, 64);
	
					for(int i = 0; i < iSize; i++)
						PushArrayString(ePerks[iPerkId][hWeaponClasses], sPieces[i]);
				
				}
			
			}
			
			
				//----[ SETTINGS ]----//
			
			if(KvJumpToKey(hKv, "settings")){
				
				KvGoBack(hKv);
			
				KvGetString(hKv, "settings", sSettingBuffer, PERK_MAX_HIGH);
				EscapeString(sSettingBuffer, ' ', '\0', ePerks[iPerkId][sPref], PERK_MAX_HIGH);
			
			}
			
			
				//----[ TAGS ]----//
			
			if(KvJumpToKey(hKv, "tags")){
				
				KvGoBack(hKv);
			
				strcopy(sTagBuffer[1], PERK_MAX_HIGH, ""); iTagSize = 0;
				KvGetString(hKv, "tags", sTagBuffer[0], PERK_MAX_VERYH);
				EscapeString(sTagBuffer[0], ' ', '\0', sTagBuffer[1], PERK_MAX_VERYH);
	
				ClearArray(ePerks[iPerkId][hTags]);
	
				if(strlen(sTagBuffer[1]) > 0){
	
					iTagSize = CountCharInString(sTagBuffer[1], '|')+1;
					char[][] sPieces = new char[iTagSize][24];
	
					ExplodeString(sTagBuffer[1], "|", sPieces, iTagSize, 24);
	
					for(int i = 0; i < iTagSize; i++)
						PushArrayString(ePerks[iPerkId][hTags], sPieces[i]);
	
				}
			
			}
			
			if(FindValueInArray(hCustomized, iPerkId) > -1)
				continue;
			
			iCustomPerkCount++;
			PushArrayCell(hCustomized, iPerkId);
		
		}while(KvGotoNextKey(hKv));
		
		PrintToServer("%s Customized %d perk%s.", CONS_PREFIX, iCustomPerkCount, iCustomPerkCount == 1 ? "" : "s");
		delete hCustomized;
	
	}
	
	if(hKv != INVALID_HANDLE)
		CloseHandle(hKv);

	PrecachePerkSounds();

}

void PrecachePerkSounds	(){

	for(int i = 0; i < g_iPerkCount; i++)
		PrecacheSound(ePerks[i][sSound]);

}

void ParseDisabledPerks	(){

	for(int i = 0; i < g_iPerkCount; i++)
		ePerks[i][bIsDisabled] = false;

	char sDisabled[2][255];
	GetConVarString(g_hCvarDisabledPerks, sDisabled[0], 255);
	EscapeString(sDisabled[0], ' ', '\0', sDisabled[1], 255);
	
	if(strlen(sDisabled[1]) == 0)
		return;

	int iDisabledNum = CountCharInString(sDisabled[1], ',') +1;
	char[][] sDisabledPieces = new char[iDisabledNum][32];
	ExplodeString(sDisabled[1], ",", sDisabledPieces, iDisabledNum, 32);

	int iPerkId = -1, iArraySize = 0;
	Handle hDisabledArray = CreateArray();
	for(int i = 0; i < iDisabledNum; i++){

		iPerkId = GetPerkOfString(sDisabledPieces[i], 32);
		if(iPerkId < 0)
			continue;

		if(FindValueInArray(hDisabledArray, iPerkId) != -1)
			continue;

		ePerks[iPerkId][bIsDisabled] = true;
		PushArrayCell(hDisabledArray, iPerkId);
		iArraySize++;

	}
	
	switch(iArraySize){
	
		case 0:{}
		
		case 1:{
	
			if(g_bCvarLog)
				LogMessage("%s Perk disabled: %s.", CONS_PREFIX, ePerks[GetArrayCell(hDisabledArray, 0)][sName]);
			
			PrintToServer("%s Perk disabled: %s.", CONS_PREFIX, ePerks[GetArrayCell(hDisabledArray, 0)][sName]);
		
		}
		
		default:{
	
			if(g_bCvarLog)
				LogMessage("%s %d perks disabled:", CONS_PREFIX, iArraySize);
			
			PrintToServer("%s %d perks disabled:", CONS_PREFIX, iArraySize);
			
			for(int i = 0; i < iArraySize; i++){
			
				if(g_bCvarLog)
					LogMessage("  • %s", ePerks[GetArrayCell(hDisabledArray, i)][sName]);
				
				PrintToServer("  > %s", ePerks[GetArrayCell(hDisabledArray, i)][sName]);
			
			}
		
		}
	
	}
	
	delete hDisabledArray;

}


//-----[ Applying ]-----//

void RollPerkForClient	(int client){

	if(!g_bCvarPluginEnabled){

		if(g_iCvarChat & CHAT_REASONS)
			PrintToChat(client, "%s %T", CHAT_PREFIX, "RTD2_Cant_Roll_Disabled", LANG_SERVER);
		
		return;

	}

	if(!IsRollerAllowed(client)){

		if(g_iCvarChat & CHAT_REASONS)
			PrintToChat(client, "%s %T", CHAT_PREFIX, "RTD2_Cant_Roll_No_Access", LANG_SERVER);
		
		return;

	}
	
	if(!IsRTDInRound()){
	
		if(g_iCvarChat & CHAT_REASONS)
			PrintToChat(client, "%s %T", CHAT_PREFIX, "RTD2_Not_In_Round", LANG_SERVER);
		
		return;
	
	}

	if(g_iCvarRtdTeam > 0 && g_iCvarRtdTeam == GetClientTeam(client)-1){

		if(g_iCvarChat & CHAT_REASONS)
			PrintToChat(client, "%s %T", CHAT_PREFIX, "RTD2_Cant_Roll_Team", LANG_SERVER);
		
		return;

	}

	if(GetForwardFunctionCount(g_hFwdCanRoll) > 0){

		Call_StartForward(g_hFwdCanRoll);
		Call_PushCell(client);
		Action result = Plugin_Continue;
		Call_Finish(result);

		if(result != Plugin_Continue)
			return;

	}

	if(!IsPlayerAlive(client)){

		if(g_iCvarChat & CHAT_REASONS)
			PrintToChat(client, "%s %T", CHAT_PREFIX, "RTD2_Cant_Roll_Alive", LANG_SERVER);
		
		return;

	}

	if(eClients[client][bRolling]){


		if(g_iCvarChat & CHAT_REASONS)
			PrintToChat(client, "%s %T", CHAT_PREFIX, "RTD2_Cant_Roll_Using", LANG_SERVER);
		
		return;

	}

	int iTimeLeft = eClients[client][iLastRoll] +g_iCvarRollInterval;
	if(GetTime() < iTimeLeft){
	
		if(g_iCvarChat & CHAT_REASONS)
			PrintToChat(client, "%s %T", CHAT_PREFIX, "RTD2_Cant_Roll_Wait", LANG_SERVER, 0x04, iTimeLeft -GetTime(), 0x01);
		
		return;

	}

	switch(g_iCvarRtdMode){

		case 1:{

			int iCount = 0;
			for(int i = 1; i <= MaxClients; i++){

				if(eClients[i][bRolling])
					iCount++;

			}

			if(iCount >= g_iCvarClientLimit){

				if(g_iCvarChat & CHAT_REASONS)
					PrintToChat(client, "%s %T", CHAT_PREFIX, "RTD2_Cant_Roll_Mode1", LANG_SERVER);
				
				return;

			}

		}

		case 2:{

			int iCount = 0, iTeam = GetClientTeam(client);
			for(int i = 1; i <= MaxClients; i++){

				if(eClients[i][bRolling])
					if(GetClientTeam(i) == iTeam)
						iCount++;

			}

			if(iCount >= g_iCvarTeamLimit){

				if(g_iCvarChat & CHAT_REASONS)
					PrintToChat(client, "%s %T", CHAT_PREFIX, "RTD2_Cant_Roll_Mode2", LANG_SERVER);
				
				return;

			}

		}

	}

	int iPerkId = RollPerk(client);
	ApplyPerk(client, iPerkId);
	
	if(g_bCvarLog)
		LogMessage("%L rolled %s(ID: %d).", client, ePerks[iPerkId][sName], iPerkId);

}

int ForcePerk			(int client, const char[] sPerkString, int iPerkStringSize=32, int iPerkTime=-1, bool bOverrideClass=false, int iGroup=-1, int initiator=0){

	if(!IsValidClient(client))
		return -4;
	
	bool bIsValidInitiator = IsValidClient(initiator);

	if(eClients[client][bRolling]){

		if(iGroup < 0)
			if(bIsValidInitiator)
				PrintToChat(initiator, "%s %N is already using RTD.", CHAT_PREFIX, client);
			else
				PrintToServer("%s %N is already using RTD.", CONS_PREFIX, client);

		return -3;

	}

	if(!IsPlayerAlive(client)){

		if(iGroup < 0)
			if(bIsValidInitiator)
				PrintToChat(initiator, "%s %N is dead.", CHAT_PREFIX, client);
			else
				PrintToServer("%s %N is already using RTD.", CONS_PREFIX, client);

		return -2;

	}

	int		iPerkId		= GetPerkOfString(sPerkString, iPerkStringSize);
	bool	bSamePerk	= true;
	int		iApplicats	= iGroup < 0 ? 1 : eGroup[iGroup][iClientCount];

	if(iPerkId < 0 || iPerkId >= g_iPerkCount){

		bSamePerk = false;

		if(!g_bTempPrint)
			if(bIsValidInitiator)
				PrintToChat(initiator, "%s Perk not found or invalid info, forcing %s.", CHAT_PREFIX, iApplicats > 1 ? "rolls" : "a roll");
			else
				PrintToServer("%s Perk not found or invalid info, forcing %s.", CONS_PREFIX, iApplicats > 1 ? "rolls" : "a roll");

		iPerkId = RollPerk(client, true, bOverrideClass, false, false, !StrEqual(sPerkString, "-"), sPerkString);

		if(iPerkId < 0){

			if(bIsValidInitiator)
				PrintToChat(initiator, "%s No perks available for %N.", CHAT_PREFIX, client);
			else
				PrintToServer("%s No perks available for %N.", CONS_PREFIX, client);

			return -1;

		}

	}

	if(bIsValidInitiator)
		if(GetForwardFunctionCount(g_hFwdCanForce) > 0){
		
			Call_StartForward(g_hFwdCanForce);
			Call_PushCell(initiator);
			Call_PushCell(client);
			Call_PushCell(iPerkId);
			Action result = Plugin_Continue;
			Call_Finish(result);
		
			if(result != Plugin_Continue)
				return -5;
		
		}

	ApplyPerk(client, iPerkId, iPerkTime, iGroup, bSamePerk ? iPerkId : -1);
	
	if(g_bCvarLog)
		if(bIsValidInitiator)
			LogMessage("A perk %s(ID: %d) has been forced on %L for %d seconds by %L.", ePerks[iPerkId][sName], iPerkId, client, iPerkTime, initiator);
		else
			LogMessage("A perk %s(ID: %d) has been forced on %L for %d seconds.", ePerks[iPerkId][sName], iPerkId, client, iPerkTime);

	return iPerkId;

}


//-----[ General ]-----//

int RollPerk	(int client=0, bool bOverrideDisabled=false, bool bOverrideClass=false, bool bCountRepeat=true, bool bCountGreatRepeat=true, bool bUseFilter=false, const char[] sTagFilter=""){

	float fGoodChance = g_fCvarGoodChance;

	if(IsValidClient(client) && IsRollerDonator(client))
		fGoodChance = g_fCvarGoodDonatorChance;

	bool bGoodPerk = fGoodChance > GetURandomFloat() ? true : false;

	Handle hAvailablePerks = CreateArray();
	for(int i = 0; i < g_iPerkCount; i++){

		if(!bUseFilter){

			if(ePerks[i][bGood] != bGoodPerk)
				continue;

			if(!bOverrideDisabled)
				if(ePerks[i][bIsDisabled])
					continue;

			if(IsValidClient(client)){

				if(bCountRepeat)
					if(!g_bCvarCanRepeatPerk)
						if(i == eClients[client][iLastPerk])
							continue;

				if(bCountGreatRepeat)
					if(!g_bCvarCanRepeatGreatPerk)
						if(i == eClients[client][iGreatLastPerk])
							continue;

			}

		}else if(!IsPerkInTags(i, sTagFilter, CountCharInString(sTagFilter, '|')+1))
			continue;

		if(!bOverrideClass)
			if(!PerkAllowedForClassOf(client, i))
				continue;

		if(!PerkAllowedForWeaponsOf(client, i))
			continue;

		PushArrayCell(hAvailablePerks, i);

	}

	int iPerkNum = GetArraySize(hAvailablePerks);

	int iUsingHandle = 0;

	Handle hAvailablePerks2 = CreateArray();
	int iPerkNum2 = -1;
	if(!iPerkNum){

		for(int i = 0; i < g_iPerkCount; i++){

			if(!bUseFilter){

				if(ePerks[i][bGood] == bGoodPerk)
					continue;

				if(!bOverrideDisabled)
					if(ePerks[i][bIsDisabled])
						continue;

				if(IsValidClient(client)){

					if(bCountRepeat)
						if(!g_bCvarCanRepeatPerk)
							if(i == eClients[client][iLastPerk])
								continue;

					if(bCountGreatRepeat)
						if(!g_bCvarCanRepeatGreatPerk)
							if(i == eClients[client][iGreatLastPerk])
								continue;

				}

			}else if(!IsPerkInTags(i, sTagFilter, CountCharInString(sTagFilter, ',')+1))
				continue;

			if(!bOverrideClass)
				if(!PerkAllowedForClassOf(client, i))
					continue;

			if(!PerkAllowedForWeaponsOf(client, i))
				continue;

			PushArrayCell(hAvailablePerks2, i);

		}

		iPerkNum2 = GetArraySize(hAvailablePerks2);

		if(iPerkNum2) iUsingHandle = 2;

	}else iUsingHandle = 1;

	int iPerkId = -1;
	switch(iUsingHandle){

		case 1:{iPerkId = GetArrayCell(hAvailablePerks, GetRandomInt(0, iPerkNum-1));}
		case 2:{iPerkId = GetArrayCell(hAvailablePerks2, GetRandomInt(0, iPerkNum2-1));}

	}

	delete hAvailablePerks;
	if(!iPerkNum) delete hAvailablePerks2;

	return iPerkId;

}

void ApplyPerk	(int client, int iPerk, int iPerkTime=-1, int iGroup=-1, int iSamePerk=-1){

	if(!IsValidClient(client)) return;
	
	EmitSoundToAll(ePerks[iPerk][sSound], client);

	ManagePerk(client, iPerk, true);

	int iDuration = -1;
	if(ePerks[iPerk][iTime] > -1){

		iDuration = (iPerkTime > -1) ? iPerkTime : (ePerks[iPerk][iTime] > 0) ? ePerks[iPerk][iTime] : g_iCvarPerkDuration;
		int iSerial = GetClientSerial(client);

		eClients[client][bRolling]	= true;
		eClients[client][iCurPerk]	= iPerk;
		eClients[client][iPerkEnd]	= GetTime() + iDuration;
		eClients[client][hPerkTimer]= CreateTimer(float(iDuration), Timer_RemovePerk, iSerial);
		DisplayPerkTimeFrame(client);
		CreateTimer(1.0, Timer_Countdown, iSerial, TIMER_REPEAT);

	}else
		eClients[client][iLastRoll]	= GetTime();

	Forward_PerkApplied(client, iPerk, iDuration);

	eClients[client][iGreatLastPerk]= eClients[client][iLastPerk];
	eClients[client][iLastPerk]		= iPerk;

	PrintToRoller(client, iPerk, iDuration);

	if(iGroup < 0 || iSamePerk < 0){

		PrintToNonRollers(client, iPerk, iDuration);

		if(iSamePerk < 0){

			g_bTempPrint = true;
			CreateTimer(0.1, Timer_ReloadTempPrint);

		}

		if(iGroup > -1)
			eGroup[iGroup][iGroupPerkId] = -1;

		return;

	}

	if(g_bTempPrint)	//----- Past this point, execution happens once -----//
		return;

	PrintGroupRolls(eGroup[iGroup][iClientCount], iSamePerk, iDuration);

	g_bTempPrint = true;
	CreateTimer(0.1, Timer_ReloadTempPrint);

	if(ePerks[iSamePerk][iTime] < 0)
		return;

	eGroup[iGroup][iGroupPerkId] = iSamePerk;
	CreateTimer(float(iDuration), Timer_PrintGroupEnd, iGroup);

}


//-----[ Descriptions ]-----//

Handle BuildDesc		(){

	Handle hMenu = CreateMenu(ManagerDesc);
	
	SetMenuTitle(hMenu, "%T", "RTD2_Menu_Title", LANG_SERVER);
	
	for(int i = 0; i < g_iCorePerkCount; i++)
		AddMenuItem(hMenu, "", ePerks[i][sName]);
	
	SetMenuExitBackButton(hMenu, false);
	SetMenuExitButton(hMenu, true);
	
	return hMenu;

}

void ShowDesc			(int client, int iPos=0){
	
	if(iPos == 0)
		DisplayMenu(g_hDescriptionMenu, client, MENU_TIME_FOREVER);
	
	else
		DisplayMenuAtItem(g_hDescriptionMenu, client, GetMenuSelectionPosition(), MENU_TIME_FOREVER);
		

}

public int ManagerDesc	(Handle hMenu, MenuAction maState, int client, int iPos){

	if(maState != MenuAction_Select)
		return 0;
	
	char sTranslatePos[16];
	Format(sTranslatePos, sizeof(sTranslatePos), "RTD2_Desc_%d", iPos);
	
	PrintToChat(client, "%s %s%s%c: \x03%T\x01", CHAT_PREFIX,
		ePerks[iPos][bGood] ? PERK_COLOR_GOOD : PERK_COLOR_BAD,
		ePerks[iPos][sName], 0x01,
		sTranslatePos, LANG_SERVER);
	
	ShowDesc(client, iPos);
	return 1;

}


//-----[ Timers ]-----//

public Action Timer_ReloadTempPrint	(Handle hTimer){

	g_bTempPrint = false;

	return Plugin_Stop;

}

public Action Timer_PrintGroupEnd	(Handle hTimer, int iGroup){

	if(!eGroup[iGroup][bActive])
		return Plugin_Stop;

	int iSize = eGroup[iGroup][iClientCount];

	if(iSize < 1)
		return Plugin_Stop;

	int iPerk = eGroup[iGroup][iGroupPerkId];

	if(iPerk > -1){

		char sReason[128];
		Format(sReason, sizeof(sReason), "%s %T", CHAT_PREFIX,
			"RTD2_Remove_Perk_Group_Same", LANG_SERVER,
			ePerks[iPerk][bGood] ? PERK_COLOR_GOOD : PERK_COLOR_BAD,
			ePerks[iPerk][sName],
			0x01,
			iSize);

		PrintToChatAll(sReason);

	}

	eGroup[iGroup][bActive] = false;

	return Plugin_Stop;

}

public Action Timer_Countdown		(Handle hTimer, int iSerial){

	int client = GetClientFromSerial(iSerial);

	if(!IsValidClient(client))
		return Plugin_Stop;
	
	if(!eClients[client][bRolling])
		return Plugin_Stop;

	DisplayPerkTimeFrame(client);

	return Plugin_Continue;

}

public Action Timer_RemovePerk		(Handle hTimer, int iSerial){

	int client = GetClientFromSerial(iSerial);
	
	if(!IsValidClient(client))
		return Plugin_Stop;
	
	if(g_bCvarLog)
		LogMessage("Perk %s(ID: %d) ended on %L.", ePerks[eClients[client][iCurPerk]][sName], eClients[client][iCurPerk], client);

	ManagePerk(client, eClients[client][iCurPerk], false);

	return Plugin_Handled;

}


//-----[ Removing ]-----//

int ForceRemovePerk	(int client, int iReason=3, const char[] sReason=""){

	if(!IsValidClient(client)) return -1;
	int iClientPerk = eClients[client][iCurPerk];

	int iGroup = eClients[client][iGroupRollId];
	if(iGroup > -1){

		if(eGroup[iGroup][bActive]){

			int iPos = FindValueInArray(eGroup[iGroup][hClientArray], GetClientSerial(client));

			if(iPos > -1){

				RemoveFromArray(eGroup[iGroup][hClientArray], iPos);
				eGroup[iGroup][iClientCount]--;

			}

			if(eGroup[iGroup][iClientCount] < 1)
				eGroup[iGroup][bActive] = false;

		}

	}

	ManagePerk(client, iClientPerk , false, iReason, sReason);

	return iClientPerk;

}

void RemovedPerk	(int client, int iReason, const char[] sReason=""){

	eClients[client][bRolling]	= false;
	eClients[client][iLastRoll]	= GetTime();

	Forward_PerkRemoved(client, eClients[client][iCurPerk], iReason);

	eClients[client][iCurPerk]	= -1;

	int iGroup = eClients[client][iGroupRollId];

	if(iGroup < 0)
		PrintPerkEndReason(client, iReason, sReason);
	else

		if(eGroup[iGroup][iGroupPerkId] < 0)
			PrintPerkEndReason(client, iReason, sReason);

	eClients[client][iGroupRollId] = -1;

	KillTimerSafe(eClients[client][hPerkTimer]);

}


//-----[ Printing ]-----//

void PrintToRoller		(int client, int iPerk, int iDuration){

	if(!(g_iCvarChat & CHAT_APPROLLER))
		return;

	if(!g_bCvarShowTime || ePerks[iPerk][iTime] == -1)
		PrintToChat(client, "%s %T", CHAT_PREFIX,
			"RTD2_Rolled_Perk_Roller", LANG_SERVER,
			ePerks[iPerk][bGood] ? PERK_COLOR_GOOD : PERK_COLOR_BAD,
			ePerks[iPerk][sName],
			0x01);
	
	else{
	
		int iTrueDuration = (iDuration > -1) ? iDuration : (ePerks[iPerk][iTime] > 0) ? ePerks[iPerk][iTime] : g_iCvarPerkDuration;
		PrintToChat(client, "%s %T", CHAT_PREFIX,
			"RTD2_Rolled_Perk_Roller_Time", LANG_SERVER,
			ePerks[iPerk][bGood] ? PERK_COLOR_GOOD : PERK_COLOR_BAD,
			ePerks[iPerk][sName],
			0x01, 0x03, iTrueDuration, 0x01);
	
	}

}

void PrintToNonRollers	(int client, int iPerk, int iDuration){

	if(!(g_iCvarChat & CHAT_APPOTHER))
		return;

	char sOthersPrint[128], sRollerName[MAX_NAME_LENGTH];
	GetClientName(client, sRollerName, sizeof(sRollerName));

	if(!g_bCvarShowTime || ePerks[iPerk][iTime] == -1)
		Format(sOthersPrint, sizeof(sOthersPrint), "%s %T", CHAT_PREFIX,
			"RTD2_Rolled_Perk_Others", LANG_SERVER,
			g_sTeamColors[GetClientTeam(client)],
			sRollerName,
			0x01,
			ePerks[iPerk][bGood] ? PERK_COLOR_GOOD : PERK_COLOR_BAD,
			ePerks[iPerk][sName], 0x01);
	
	else{
	
		int iTrueDuration = (iDuration > -1) ? iDuration : (ePerks[iPerk][iTime] > 0) ? ePerks[iPerk][iTime] : g_iCvarPerkDuration;
		Format(sOthersPrint, sizeof(sOthersPrint), "%s %T", CHAT_PREFIX,
			"RTD2_Rolled_Perk_Others_Time", LANG_SERVER,
			g_sTeamColors[GetClientTeam(client)],
			sRollerName,
			0x01,
			ePerks[iPerk][bGood] ? PERK_COLOR_GOOD : PERK_COLOR_BAD,
			ePerks[iPerk][sName], 0x01, 0x03, iTrueDuration, 0x01);
	
	}

	PrintToChatAllExcept(client, sOthersPrint);

}

void PrintGroupRolls	(int iApplications, int iPerk, int iDuration){

	if(!(g_iCvarChat & CHAT_APPOTHER))
		return;

	char sReason[128];

	if(!g_bCvarShowTime || ePerks[iPerk][iTime] == -1)
		Format(sReason, sizeof(sReason), "%s %T", CHAT_PREFIX,
			"RTD2_Rolled_Perk_Multi", LANG_SERVER,
			iApplications,
			ePerks[iPerk][bGood] ? PERK_COLOR_GOOD : PERK_COLOR_BAD,
			ePerks[iPerk][sName],
			0x01);
	
	else{
	
		int iTrueDuration = (iDuration > -1) ? iDuration : (ePerks[iPerk][iTime] > 0) ? ePerks[iPerk][iTime] : g_iCvarPerkDuration;
		Format(sReason, sizeof(sReason), "%s %T", CHAT_PREFIX,
			"RTD2_Rolled_Perk_Multi_Time", LANG_SERVER,
			iApplications,
			ePerks[iPerk][bGood] ? PERK_COLOR_GOOD : PERK_COLOR_BAD,
			ePerks[iPerk][sName],
			0x01, 0x03, iTrueDuration, 0x01);
	
	}

	PrintToChatAll(sReason);

}

void PrintPerkEndReason	(int client, int iReason=3, const char[] sCustomReason=""){

	char sReasonSelf[32], sReasonOthers[32];
	switch(iReason){

		case 0:{

			strcopy(sReasonSelf, sizeof(sReasonSelf), "RTD2_Remove_Perk_Unload_Self");
			strcopy(sReasonOthers, sizeof(sReasonOthers), "RTD2_Remove_Perk_Unload_Others");

		}

		case 1:{

			strcopy(sReasonSelf, sizeof(sReasonSelf), "RTD2_Remove_Perk_Died_Self");
			strcopy(sReasonOthers, sizeof(sReasonOthers), "RTD2_Remove_Perk_Died_Others");

		}

		case 2:{

			strcopy(sReasonSelf, sizeof(sReasonSelf), "RTD2_Remove_Perk_Class_Self");
			strcopy(sReasonOthers, sizeof(sReasonOthers), "RTD2_Remove_Perk_Class_Others");

		}

		case 3:{

			strcopy(sReasonSelf, sizeof(sReasonSelf), "RTD2_Remove_Perk_End_Self");
			strcopy(sReasonOthers, sizeof(sReasonOthers), "RTD2_Remove_Perk_End_Others");

		}

		case 4:{

			strcopy(sReasonSelf, sizeof(sReasonSelf), "0");
			strcopy(sReasonOthers, sizeof(sReasonOthers), "RTD2_Remove_Perk_Disconnected");

		}

		case 5:{

			strcopy(sReasonSelf, sizeof(sReasonSelf), "RTD2_Remove_Perk_Custom_Self");
			strcopy(sReasonOthers, sizeof(sReasonOthers), "RTD2_Remove_Perk_Custom_Others");

		}

	}

	if(sReasonSelf[0] != '0' && (g_iCvarChat & CHAT_REMROLLER))
		PrintToChat(client, "%s %T", CHAT_PREFIX, sReasonSelf, LANG_SERVER, iReason > 4 ? sCustomReason : "");

	if(!(g_iCvarChat & CHAT_REMOTHER))
		return;

	char sPrintReasonToOthers[128];
	Format(sPrintReasonToOthers, sizeof(sPrintReasonToOthers), "%s %T", CHAT_PREFIX, sReasonOthers, LANG_SERVER, g_sTeamColors[GetClientTeam(client)], client, 0x01, iReason > 4 ? sCustomReason : "");
	PrintToChatAllExcept(client, sPrintReasonToOthers);

}



			//*************************//
			//----  N A T I V E S  ----//
			//*************************//

public int Native_GetClientPerkId	(Handle hPlugin, int iParams){

	return eClients[GetNativeCell(1)][iCurPerk];

}

public int Native_GetClientPerkTime	(Handle hPlugin, int iParams){

	int client = GetNativeCell(1);
	return eClients[client][bRolling] ? eClients[client][iPerkEnd] -GetTime() : -1;

}

public int Native_ForcePerk			(Handle hPlugin, int iParams){

	char sPerkString[32]; int iStringSize = sizeof(sPerkString);
	GetNativeString(2, sPerkString, iStringSize);

	return ForcePerk(
		GetNativeCell(1),
		sPerkString,
		iStringSize,
		GetNativeCell(3),
		GetNativeCell(4) > 0 ? true : false,
		GetNativeCell(5)
	);

}

public int Native_RollPerk			(Handle hPlugin, int iParams){

	return RollPerk(
		GetNativeCell(1),
		GetNativeCell(2) > 0 ? true : false,
		GetNativeCell(3) > 0 ? true : false,
		GetNativeCell(4) > 0 ? true : false,
		GetNativeCell(5) > 0 ? true : false
	);

}

public int Native_RemovePerk		(Handle hPlugin, int iParams){

	char sReason[32]; GetNativeString(3, sReason, sizeof(sReason));
	int client = GetNativeCell(1);

	Forward_OnRemovePerkPre(client);

	if(!eClients[client][bRolling])
		return -1;

	return ForceRemovePerk(
		client,
		GetNativeCell(2),
		sReason
	);

}

public int Native_GetPerkOfString	(Handle hPlugin, int iParams){

	char sString[32]; int iSize = sizeof(sString);
	GetNativeString(1, sString, iSize);

	return GetPerkOfString(sString, iSize);

}

public int Native_RegisterPerk		(Handle hPlugin, int iParams){

	char sPluginName[32];
	GetPluginFilename(hPlugin, sPluginName, sizeof(sPluginName));

	if(!g_bIsRegisteringOpen){
	
		ThrowNativeError(SP_ERROR_NATIVE, "%s Plugin \"%s\" is trying to register perks before it's possible.\nPlease use the forward RTD2_OnRegOpen() and native RTD2_IsRegOpen() to determine.", CONS_PREFIX, sPluginName);
		return -1;
	
	}

	if(g_iPerkCount >= PERK_MAX_COUNT-1){
	
		ThrowNativeError(SP_ERROR_NATIVE, "%s No space for new perks.\nPlease recompile the core increasing \"PERK_MAX_COUNT\" define.", CONS_PREFIX);
		return -1;
	
	}

	char sTokenBuffer[2][PERK_MAX_LOW], sClassBuffer[2][PERK_MAX_LOW], sWeaponsBuffer[2][PERK_MAX_HIGH], sTagsBuffer[2][PERK_MAX_VERYH];
	
	
		//---[ Token ]---//
	
	GetNativeString(1, sTokenBuffer[0], PERK_MAX_LOW);
	EscapeString(sTokenBuffer[0], ' ', '\0', sTokenBuffer[1], PERK_MAX_LOW);
	
	int iPerkId = FindPerkByToken(sTokenBuffer[1]);
	if(iPerkId == -1){
	
		iPerkId = g_iPerkCount;
		g_iPerkCount++;
	
	}
	
	strcopy(ePerks[iPerkId][sToken], PERK_MAX_LOW, sTokenBuffer[1]);
	
	
		//---[ Name ]---//
	
	GetNativeString(2, ePerks[iPerkId][sName], PERK_MAX_LOW);
	
	
		//---[ Good ]---//
	
	ePerks[iPerkId][bGood] = GetNativeCell(3) > 0 ? true : false;
	
	
		//---[ Sound ]---//
	
	GetNativeString(4, ePerks[iPerkId][sSound], PERK_MAX_HIGH);
	PrecacheSound(ePerks[iPerkId][sSound]);
	
	
		//---[ Time ]---//
	
	ePerks[iPerkId][iTime] = GetNativeCell(5);
	
	
		//---[ Class ]---//
	
	strcopy(sClassBuffer[1], PERK_MAX_LOW, "");
	GetNativeString(6, sClassBuffer[0], PERK_MAX_LOW);
	EscapeString(sClassBuffer[0], ' ', '\0', sClassBuffer[1], PERK_MAX_LOW);
	
	int iClassFlags = ClassStringToFlags(sClassBuffer[1]);
	if(iClassFlags < 1){
	
		PrintToServer("%s WARNING: A plugin \"%s\" is registering a perk with invalid class restriction(s) for perk \"%s\". Assuming it's all-class.", CONS_PREFIX, sPluginName, ePerks[g_iPerkCount][sName]);
		LogError("%s WARNING: A plugin \"%s\" is registering a perk with invalid class restriction(s) for perk \"%s\". Assuming it's all-class.", CONS_PREFIX, sPluginName, ePerks[g_iPerkCount][sName]);
		iClassFlags = 511;

	}
	
	ePerks[iPerkId][iClasses] = iClassFlags;
	
	
		//---[ Weapons ]---//
	
	strcopy(sWeaponsBuffer[1], PERK_MAX_HIGH, "");
	GetNativeString(7, sWeaponsBuffer[0], PERK_MAX_HIGH);
	EscapeString(sWeaponsBuffer[0], ' ', '\0', sWeaponsBuffer[1], PERK_MAX_HIGH);
	
	if(ePerks[iPerkId][hWeaponClasses] == INVALID_HANDLE)
		ePerks[iPerkId][hWeaponClasses] = CreateArray(32);
	else
		ClearArray(ePerks[iPerkId][hWeaponClasses]);
	
	if(FindCharInString(sWeaponsBuffer[1], '0') < 0){
		
		int iSize = CountCharInString(sWeaponsBuffer[1], ',')+1;
		char[][] sPieces = new char[iSize][32];
		
		ExplodeString(sWeaponsBuffer[1], ",", sPieces, iSize, 64);
		
		for(int i = 0; i < iSize; i++)
			PushArrayString(ePerks[iPerkId][hWeaponClasses], sPieces[i]);
	
	}
	
	
		//---[ Tags ]---//
	
	strcopy(sTagsBuffer[1], PERK_MAX_VERYH, "");
	GetNativeString(8, sTagsBuffer[0], PERK_MAX_VERYH);
	EscapeString(sTagsBuffer[0], ' ', '\0', sTagsBuffer[1], PERK_MAX_VERYH);
	
	if(ePerks[iPerkId][hTags] == INVALID_HANDLE)
		ePerks[iPerkId][hTags] = CreateArray(32);
	else
		ClearArray(ePerks[iPerkId][hTags]);
		
	if(strlen(sTagsBuffer[1]) > 0){
	
		int iTagSize = CountCharInString(sTagsBuffer[1], '|')+1;
		char[][] sPieces = new char[iTagSize][24];
		
		ExplodeString(sTagsBuffer[1], "|", sPieces, iTagSize, 24);
		
		for(int i = 0; i < iTagSize; i++)
			PushArrayString(ePerks[iPerkId][hTags], sPieces[i]);
		
	}
	
	
		//---[ The Rest ]---//
	
	ePerks[iPerkId][bIsExternal]	= true;
	ePerks[iPerkId][funcCallback]	= GetNativeCell(9);
	ePerks[iPerkId][plParent]		= hPlugin;
	
	return iPerkId;

}

public int Native_IsRegisteringOpen	(Handle hPlugin, int iParams){

	return g_bIsRegisteringOpen;

}

public int Native_SetPerkByToken	(Handle hPlugin, int iParams){

	char sTokenBuffer[PERK_MAX_LOW];
	GetNativeString(1, sTokenBuffer, PERK_MAX_LOW);

	int iPerkId = FindPerkByToken(sTokenBuffer);
	
	if(iPerkId == -1)
		return -1;
	
	int iDir = GetNativeCell(2);
	
	if(iDir < -1)
		iDir = -1;
	else if(iDir > 1)
		iDir = 1;
	
	switch(iDir){
	
		case -1:ePerks[iPerkId][bIsDisabled] = true;
		case 0:	ePerks[iPerkId][bIsDisabled] = ePerks[iPerkId][bIsDisabled] ? false : true;
		case 1:	ePerks[iPerkId][bIsDisabled] = false;
	
	}
	
	return iPerkId;

}

public int Native_SetPerkById		(Handle hPlugin, int iParams){

	int iPerkId = GetNativeCell(1);
	
	if(iPerkId < 0 || iPerkId >= g_iPerkCount)
		return -1;
	
	int iDir = GetNativeCell(2);
	
	if(iDir < -1)
		iDir = -1;
	else if(iDir > 1)
		iDir = 1;
	
	int iChange = 0;
	
	switch(iDir){
	
		case -1:{
	
			if(!ePerks[iPerkId][bIsDisabled]){
			
				ePerks[iPerkId][bIsDisabled] = true;
				iChange = 1;
			
			}

		}
	
		case 0:{
		
			ePerks[iPerkId][bIsDisabled] = ePerks[iPerkId][bIsDisabled] ? false : true;
			iChange = 1;

		}
	
		case 1:{
	
			if(ePerks[iPerkId][bIsDisabled]){
			
				ePerks[iPerkId][bIsDisabled] = false;
				iChange = 1;
			
			}

		}
	
	}
	
	return iChange;

}

public int Native_DefaultCorePerk	(Handle hPlugin, int iParams){

	int iPerkId = GetNativeCell(1);
	
	if(iPerkId < 0 || iPerkId >= g_iCorePerkCount){
	
		char sTokenBuffer[PERK_MAX_LOW];
		GetNativeString(2, sTokenBuffer, PERK_MAX_LOW);
	
		if(strlen(sTokenBuffer) < 1)
			return -1;
		
		iPerkId = FindPerkByToken(sTokenBuffer);
		
		if(iPerkId == -1)
			return -1;
	
	}
	
	int iChange = 0;
	if(ePerks[iPerkId][bIsExternal]){
	
		iChange = 1;
		ePerks[iPerkId][bIsExternal] = false;
	
	}
	
	return iChange;

}

public int Native_CanPlayerBeHurt	(Handle hPlugin, int iParams){

	int client = GetNativeCell(1);
	
	if(client < 1 || client > MaxClients)
		return 0;
	
	if(!IsClientInGame(client))
		return 0;
	
	return view_as<int>(CanPlayerBeHurt(client, GetNativeCell(2)));

}



			//***************************//
			//----  F O R W A R D S  ----//
			//***************************//

void Forward_PerkApplied	(int client, int iPerk, int iDuration){

	if(GetForwardFunctionCount(g_hFwdRolled) < 1)
		return;

	Call_StartForward(g_hFwdRolled);
	Call_PushCell(client);
	Call_PushCell(iPerk);
	Call_PushCell(iDuration);
	Call_Finish();

}

void Forward_PerkRemoved	(int client, int iPerk, int iReason){

	if(GetForwardFunctionCount(g_hFwdRemoved) < 1)
		return;

	Call_StartForward(g_hFwdRemoved);
	Call_PushCell(client);
	Call_PushCell(iPerk);
	Call_PushCell(iReason);
	Call_Finish();

}

void Forward_OnRegOpen		(){

	if(GetForwardFunctionCount(g_hFwdOnRegOpen) < 1)
		return;

	Call_StartForward(g_hFwdOnRegOpen);
	Call_Finish();

}



			//***********************//
			//----  S T O C K S  ----//
			//***********************//

//-----[ Strings ]-----//

stock bool PerkAllowedForClassOf	(int client, int iPerkId){

	switch(TF2_GetPlayerClass(client)){

		case TFClass_Scout:		{if(ePerks[iPerkId][iClasses] & 1)		return true;}
		case TFClass_Soldier:	{if(ePerks[iPerkId][iClasses] & 2)		return true;}
		case TFClass_Pyro:		{if(ePerks[iPerkId][iClasses] & 4)		return true;}
		case TFClass_DemoMan:	{if(ePerks[iPerkId][iClasses] & 8)		return true;}
		case TFClass_Heavy:		{if(ePerks[iPerkId][iClasses] & 16)		return true;}
		case TFClass_Engineer:	{if(ePerks[iPerkId][iClasses] & 32)		return true;}
		case TFClass_Medic:		{if(ePerks[iPerkId][iClasses] & 64)		return true;}
		case TFClass_Sniper:	{if(ePerks[iPerkId][iClasses] & 128)	return true;}
		case TFClass_Spy:		{if(ePerks[iPerkId][iClasses] & 256)	return true;}

	}

	return false;

}

stock bool PerkAllowedForWeaponsOf	(int client, int iPerkId){

	if(ePerks[iPerkId][hWeaponClasses] == INVALID_HANDLE)
		return true;

	int iSize = GetArraySize(ePerks[iPerkId][hWeaponClasses]);

	if(iSize < 1)
		return true;

	char sClass[32], sWeapClass[32];
	int iWeapon = 0;
	for(int i = 0; i < 5; i++){

		iWeapon = GetPlayerWeaponSlot(client, i);

		if(iWeapon <= MaxClients)
			continue;

		if(!IsValidEntity(iWeapon))
			continue;

		GetEntityClassname(iWeapon, sWeapClass, 32);

		for(int j = 0; j < iSize; j++){

			GetArrayString(ePerks[iPerkId][hWeaponClasses], j, sClass, 32);
			if(StrContains(sWeapClass, sClass, false) > -1)
				return true;

		}

	}

	return false;

}

stock bool IsPerkInTags				(int iPerkId, const char[] sTagString, iTagNum){

	char[][] sPieces = new char[iTagNum][32];
	ExplodeString(sTagString, "|", sPieces, iTagNum, 32);

	int iPerkTags = GetArraySize(ePerks[iPerkId][hTags]);
	char sThisTag[16];

	for(int i = 0; i < iTagNum; i++)
		for(int j = 0; j < iPerkTags; j++){

			GetArrayString(ePerks[iPerkId][hTags], j, sThisTag, 16);
			if(StrEqual(sThisTag, sPieces[i], false))
				return true;

		}

	return false;

}

stock int CountTagOccurences		(const char[] sTag, int iTagSize){

	int count = 0;
	for(int i = 0; i < g_iPerkCount; i++){

		if(IsPerkInTags(i, sTag, 1))
			count++;

	}

	return count;

}

stock int CountCharInString			(const char[] sString, char cChar){

	int i = 0, count = 0;
	
	while(sString[i] != '\0')
		if(sString[i++] == cChar)
			count++;

	return count;

}

stock int EscapeString				(const char[] input, int escape, int escaper, char[] output, int maxlen){

	/*
		Thanks Popoklopsi for EscapeString()
		https://forums.alliedmods.net/showthread.php?t=212230
	*/

	// Number of chars we escaped
	int escaped = 0;

	// Format output buffer to ""
	Format(output, maxlen, "");


	// For each char in the input string
	for(int offset = 0; offset < strlen(input); offset++){

		// Get char at the current position
		int ch = input[offset];

		// Found the escape or escaper char
		if(ch == escape || ch == escaper){

			// Escape the escape char with the escaper^^
			Format(output, maxlen, "%s%c%c", output, escaper, ch);

			// Increase numbers of chars we escaped
			escaped++;

		}else
			// Add other char to output buffer
			Format(output, maxlen, "%s%c", output, ch);
	}

	// Return escaped chars
	return escaped;

}


//-----[ Feedback ]-----//

stock void PrintToChatAllExcept	(int client, char[] sMessage){

	for(int i = 1; i <= MaxClients; i++){

		if(!IsValidClient(i) || i == client) continue;

		PrintToChat(i, sMessage);

	}

}

stock void DisplayPerkTimeFrame	(client){

	int iTeam	= GetClientTeam(client);
	int iRed	= (iTeam == 2) ? 255 : 32;
	int iBlue	= (iTeam == 3) ? 255 : 32;

	SetHudTextParams(g_fCvarTimerPosX, g_fCvarTimerPosY, 1.0, iRed, 32, iBlue, 255);
	ShowSyncHudText(client, eClients[client][hHudSync], "%s: %d", ePerks[eClients[client][iCurPerk]][sName], eClients[client][iPerkEnd] -GetTime());

}


//-----[ Perks ]-----//

stock int ClassStringToFlags	(char[] sClasses){

	if(FindCharInString(sClasses, '0') > -1)
		return 511;

	int iLength = strlen(sClasses);
	if(iLength < 2){
		int iClass = StringToInt(sClasses);
		if(iClass < 1)
			return 0;
		else
			return iPow(2, iClass-1);
	}else{

		int iCharSize = (iLength+1)/2;
		char[][] sPieces = new char[iCharSize][4];
		ExplodeString(sClasses, ",", sPieces, iCharSize, 4);

		int iValue = 0, iPowed = 0, iFlags = 0;
		for(int i = 0; i < iCharSize; i++){

			iValue = StringToInt(sPieces[i]);
			if(iValue > 9)		continue;

			iPowed = iPow(2, iValue-1);
			if(iFlags & iPowed)	continue;

			iFlags |= iPowed;

		}

		return iFlags;

	}

}

stock int GetPerkOfString		(const char[] sString, int iStringSize){

	int iString = StringToInt(sString);
	if(!(FindCharInString(sString, '0') < 0 && iString == 0))
		return iString;

	for(int i = 0; i < g_iPerkCount; i++){

		if(StrEqual(sString, ePerks[i][sToken]))
			return i;

	}

	if(CountTagOccurences(sString, iStringSize) == 1)
		for(int j = 0; j < g_iPerkCount; j++){

			if(IsPerkInTags(j, sString, 1))
				return j;

		}

	return -1;

}

stock int GetPerkTime			(int iPerkId){

	return (ePerks[iPerkId][iTime] > 0) ? ePerks[iPerkId][iTime] : g_iCvarPerkDuration;

}

stock int GetNextAvailableGroup	(){

	for(int i = 0; i <= MaxClients; i++)
		if(!eGroup[i][bActive])
			return i;

	return -1;

}


//-----[ Miscellaneous ]-----//

stock int ReadFlagFromConVar	(Handle hCvar){

	char sBuffer[32];
	GetConVarString(hCvar, sBuffer, sizeof(sBuffer));

	return ReadFlagString(sBuffer);

}

stock int ConnectWithBeam		(int iEnt, int iEnt2, int iRed=255, int iGreen=255, int iBlue=255, float fStartWidth=1.0, float fEndWidth=1.0, float fAmp=1.35){

	int iBeam = CreateEntityByName("env_beam");
	
	if(iBeam <= MaxClients)
		return -1;
	
	if(!IsValidEntity(iBeam))
		return -1;
	
	SetEntityModel(iBeam, LASERBEAM);
	
	char sColor[16];
	Format(sColor, sizeof(sColor), "%d %d %d", iRed, iGreen, iBlue);
	
	DispatchKeyValue(iBeam, "rendercolor", sColor);
	DispatchKeyValue(iBeam, "life", "0");
	
	DispatchSpawn(iBeam);
	
	SetEntPropEnt(iBeam, Prop_Send, "m_hAttachEntity", EntIndexToEntRef(iEnt));
	SetEntPropEnt(iBeam, Prop_Send, "m_hAttachEntity", EntIndexToEntRef(iEnt2), 1);
	
	SetEntProp(iBeam, Prop_Send, "m_nNumBeamEnts", 2);
	SetEntProp(iBeam, Prop_Send, "m_nBeamType", 2);
	
	SetEntPropFloat(iBeam, Prop_Data, "m_fWidth", 1.0);
	SetEntPropFloat(iBeam, Prop_Data, "m_fEndWidth", 1.0);
	
	SetEntPropFloat(iBeam, Prop_Data, "m_fAmplitude", 1.35);
	
	SetVariantFloat(32.0);
	AcceptEntityInput(iBeam, "Amplitude");
	AcceptEntityInput(iBeam, "TurnOn");
	
	return iBeam;

}

stock int CreateParticle		(int iClient, char[] strParticle, bool bAttach=true, char[] strAttachmentPoint="", float fOffset[3]={0.0, 0.0, 36.0}){

	//Thanks J-Factor for CreateParticle()

	int iParticle = CreateEntityByName("info_particle_system");
	if(!IsValidEdict(iParticle)) return 0;

	float fPosition[3], fAngles[3], fForward[3], fRight[3], fUp[3];

	// Retrieve entity's position and angles
	GetClientAbsOrigin(iClient, fPosition);
	GetClientAbsAngles(iClient, fAngles);

	// Determine vectors and apply offset
	GetAngleVectors(fAngles, fForward, fRight, fUp);    // I assume 'x' is Right, 'y' is Forward and 'z' is Up
	fPosition[0] += fRight[0]*fOffset[0] + fForward[0]*fOffset[1] + fUp[0]*fOffset[2];
	fPosition[1] += fRight[1]*fOffset[0] + fForward[1]*fOffset[1] + fUp[1]*fOffset[2];
	fPosition[2] += fRight[2]*fOffset[0] + fForward[2]*fOffset[1] + fUp[2]*fOffset[2];

	// Teleport and attach to client
	TeleportEntity(iParticle, fPosition, fAngles, NULL_VECTOR);
	DispatchKeyValue(iParticle, "effect_name", strParticle);

	if(bAttach){

		SetVariantString("!activator");
		AcceptEntityInput(iParticle, "SetParent", iClient, iParticle, 0);

		if(!StrEqual(strAttachmentPoint, "")){

			SetVariantString(strAttachmentPoint);
			AcceptEntityInput(iParticle, "SetParentAttachmentMaintainOffset", iParticle, iParticle, 0);

		}

	}

	// Spawn and start
	DispatchSpawn(iParticle);
	ActivateEntity(iParticle);
	AcceptEntityInput(iParticle, "Start");

	return iParticle;

}

stock void FixPotentialStuck	(int client){

	if(!g_bCvarRespawnStuck)
		return;

	if(client < 1 || client > MaxClients)
		return;

	if(!IsClientInGame(client))
		return;
	
	CreateTimer(0.1, Timer_FixStuck, GetClientSerial(client));

}

public Action Timer_FixStuck	(Handle hTimer, int iSerial){

	int client = GetClientFromSerial(iSerial);
	
	if(client < 1 || client > MaxClients)
		return Plugin_Stop;
	
	if(!IsClientInGame(client))
		return Plugin_Stop;
	
	if(!IsPlayerAlive(client))
		return Plugin_Stop;
	
	if(!IsEntityStuck(client))
		return Plugin_Stop;
	
	PrintToChat(client, "%s %T", CHAT_PREFIX, "RTD2_Stuck_Respawn", LANG_SERVER);
	TF2_RespawnPlayer(client);
	
	return Plugin_Stop;

}


//-----[ Checks ]-----//

stock int FindPerkByToken		(const char[] sCheckToken){

	for(int i = 0; i < g_iPerkCount; i++)
		if(StrEqual(sCheckToken, ePerks[i][sToken], false))
			return i;
	
	return -1;

}

stock bool IsArgumentTrigger	(const char[] sArg){

	char sTrigger[16];
	for(int i = 0; i < g_iCvarTriggers; i++){

		GetArrayString(g_arrCvarTriggers, i, sTrigger, 16);
		if(StrEqual(sArg, sTrigger, false))
			return true;

	}

	return false;

}

stock bool IsEntityStuck		(int iEntity){

	float fPos[3], fMins[3], fMaxs[3];
	GetEntPropVector(iEntity, Prop_Send, "m_vecOrigin", fPos);
	GetEntPropVector(iEntity, Prop_Send, "m_vecMins", fMins);
	GetEntPropVector(iEntity, Prop_Send, "m_vecMaxs", fMaxs);

	TR_TraceHullFilter(fPos, fPos, fMins, fMaxs, MASK_SOLID, TraceFilterIgnoreSelf, iEntity);
	return TR_DidHit();

}

stock bool IsRollerAllowed		(int client){

	if(g_iCvarAllowed > 0)
		return view_as<bool>(GetUserFlagBits(client) & g_iCvarAllowed);

	return true;

}

stock bool IsRollerDonator		(int client){

	if(g_iCvarDonatorFlag > 0)
		return view_as<bool>(GetUserFlagBits(client) & g_iCvarDonatorFlag);

	return false;

}

stock bool CanBuildAtPos		(float fPos[3], bool bSentry){

	//TODO: Figure out a neat way of checking nobuild areas. I've spent 5h non stop trying to do it, help pls.
	
	float fMins[3], fMaxs[3];
	if(bSentry){

		fMins[0] = -20.0;
		fMins[1] = -20.0;
		fMins[2] = 0.0;

		fMaxs[0] = 20.0;
		fMaxs[1] = 20.0;
		fMaxs[2] = 66.0;

	}else{

		fMins[0] = -24.0;
		fMins[1] = -24.0;
		fMins[2] = 0.0;

		fMaxs[0] = 24.0;
		fMaxs[1] = 24.0;
		fMaxs[2] = 55.0;

	}

	TR_TraceHull(fPos, fPos, fMins, fMaxs, MASK_SOLID);
	return !TR_DidHit();

}

stock bool CanPlayerBeHurt		(int client, int by=0){

	if(IsValidClient(by))
		if(GetClientTeam(by) == GetClientTeam(client))
			return false;

	if(IsPlayerFriendly(client))
		return false;

	if(TF2_IsPlayerInCondition(client, TFCond_Ubercharged))
		return false;

	if(GetEntProp(client, Prop_Data, "m_takedamage") != 2)
		return false;

	return true;

}

stock bool IsPlayerFriendly		(int client){

	if(g_bPluginFriendly)
		if(TF2Friendly_IsFriendly(client))
			return true;

	if(g_bPluginFriendlySimple)
		if(FriendlySimple_IsFriendly(client))
			return true;

	return false;

}

stock bool CanEntitySeeTarget	(int entity, int iTarget){

	float fStart[3], fEnd[3];

	if(IsValidClient(entity))
		GetClientEyePosition(entity, fStart);
	else
		GetEntPropVector(entity, Prop_Send, "m_vecOrigin", fStart);

	if(IsValidClient(iTarget))
		GetClientEyePosition(iTarget, fEnd);
	else
		GetEntPropVector(iTarget, Prop_Send, "m_vecOrigin", fEnd);

	Handle hTrace = TR_TraceRayFilterEx(fStart, fEnd, MASK_SOLID, RayType_EndPoint, TraceFilterIgnorePlayersAndSelf, entity);
	if(hTrace != INVALID_HANDLE){

		if(TR_DidHit(hTrace)){

			CloseHandle(hTrace);
			return false;

		}

		CloseHandle(hTrace);

	}

	return true;

}

stock bool IsRTDInRound			(){

	if(GameRules_GetProp("m_bInWaitingForPlayers", 1))
		return false;

	if(!g_bCvarInSetup){
	
		if(g_bIsGameArena && GameRules_GetRoundState() != view_as<RoundState>(7))
			return false;
	
		if(GameRules_GetProp("m_bInSetup", 1))
			return false;
	
	}
	
	return true;

}


//-----[ Trace ]-----//

public bool TraceFilterIgnoreSelf			(int iEntity, int iContentsMask, any iTarget){

	if(iEntity == iTarget)
		return false;

	return true;

}

public bool TraceFilterIgnorePlayers		(int iEntity, int iContentsMask, any data){

	if(iEntity >= 1 && iEntity <= MaxClients)
		return false;

	return true;

}

public bool TraceFilterIgnorePlayersAndSelf	(int iEntity, int iContentsMask, any iTarget){

	if(iEntity >= 1 && iEntity <= MaxClients)
		return false;

	if(iEntity == iTarget)
		return false;

	return true;

}

stock bool GetClientLookPosition			(int client, float fPosition[3]){

	float fPos[3], fAng[3];
	GetClientEyePosition(client, fPos);
	GetClientEyeAngles(client, fAng);

	Handle hTrace = TR_TraceRayFilterEx(fPos, fAng, MASK_SHOT, RayType_Infinite, TraceFilterIgnorePlayers, client);
	if(hTrace != INVALID_HANDLE && TR_DidHit(hTrace)){

		TR_GetEndPosition(fPosition, hTrace);
		return true;

	}

	return false;

}


//-----[ Helpers ]-----//

stock int AccountIDToClient		(int iAccountID){

	for(int i = 1; i <= MaxClients; i++)
		if(IsClientInGame(i))
			if(GetSteamAccountID(i) == iAccountID)
				return i;
	
	return -1;

}

stock void KillTimerSafe		(Handle &hTimer){

	if(hTimer == INVALID_HANDLE)
		return;

	KillTimer(hTimer);
	hTimer = INVALID_HANDLE;

}

stock int iPow					(int iValue, int iExponent){

	//Thanks, D.Moder
	return RoundFloat(Pow(float(iValue), float(iExponent)));

}

public bool IsValidClient		(int client){

	if(client > 4096){
		client = EntRefToEntIndex(client);
	}

	if(client < 1 || client > MaxClients)				return false;

	if(!IsClientInGame(client))							return false;

	if(IsFakeClient(client))							return false;

	if(GetEntProp(client, Prop_Send, "m_bIsCoaching"))	return false;

	return true;

}