#include <amxmodx>
#include <amxmisc>
#include <engine>
#include <fun>
#include <cstrike>

#pragma semicolon 1;

#define PLUGIN "Block Maker"
#define VERSION "3.51"
#define AUTHOR "Necro"
#define MAIN_MENU_KEYS			(1<<0)|(1<<1)|(1<<2)|(1<<3)|(1<<4)|(1<<5)|(1<<6)|(1<<7)|(1<<8)|(1<<9)
#define BLOCKSELECTION_MENU_KEYS	(1<<0)|(1<<1)|(1<<2)|(1<<3)|(1<<4)|(1<<5)|(1<<6)|(1<<7)|(1<<8)|(1<<9)
#define OPTIONS_MENU_KEYS		(1<<0)|(1<<1)|(1<<2)|(1<<3)|(1<<4)|(1<<5)|(1<<6)|(1<<7)|(1<<8)|(1<<9)
#define TELEPORT_MENU_KEYS		(1<<0)|(1<<1)|(1<<2)|(1<<3)|(1<<5)|(1<<6)|(1<<7)|(1<<9)
#define CHOICE_MENU_KEYS		(1<<0)|(1<<1)|(1<<9)
#define BM_ADMIN_LEVEL 			ADMIN_MENU	//admin access level to use this plugin. ADMIN_MENU = flag 'u'

const Float:gfSnapDistance = 10.0;	//blocks snap together when they're within this value + the players snap gap

/* enum for menu option values */
enum
{
	N1, N2, N3, N4, N5, N6, N7, N8, N9, N0
};

/* enum for options with YES/NO confirmation */
enum
{
	CHOICE_LOAD,
	CHOICE_DEL_BLOCKS,
	CHOICE_DEL_TELEPORTS
};

/* hud message values */
const gHudRed = 10;
const gHudGreen = 30;
const gHudBlue = 200;
const Float:gfTextX = -1.0;
const Float:gfTextY = 0.84;
const gHudEffects = 0;
const Float:gfHudFxTime = 0.0;
const Float:gfHudHoldTime = 0.25;
const Float:gfHudFadeInTime = 0.0;
const Float:gfHudFadeOutTime = 0.0;
const gHudChannel = 2;

/* Task ID offsets */
const TASK_GRAB = 1000;
const TASK_BHOPSOLID = 2000;
const TASK_BHOPSOLIDNOT = 3000;
const TASK_INVINCIBLE = 4000;
const TASK_STEALTH = 5000;
const TASK_ICE = 6000;
const TASK_SPRITE = 7000;
const TASK_CAMOUFLAGE = 8000;
const TASK_HONEY = 9000;
const TASK_FIRE = 10000;
const TASK_BOOTSOFSPEED = 11000;
const TASK_TELEPORT = 12000;

/* Strings */
new const gszPrefix[] = "[BM] ";
new const gszInfoTarget[] = "info_target";
new const gszHelpFilenameFormat[] = "blockmaker_v%s.txt";
new gszFile[128];
new gszNewFile[128];
new gszMenu[256];
new gszOptionsMenu[256];
new gszTeleportMenu[256];
new gszChoiceMenu[128];
new gszHelpTitle[64];
new gszHelpText[1600];
new gszHelpFilename[32];
new gszViewModel[33][32];

/* Block dimensions */
new Float:gfBlockSizeMinForX[3] = {-4.0,-32.0,-32.0};
new Float:gfBlockSizeMaxForX[3] = { 4.0, 32.0, 32.0};
new Float:gfBlockSizeMinForY[3] = {-32.0,-4.0,-32.0};
new Float:gfBlockSizeMaxForY[3] = { 32.0, 4.0, 32.0};
new Float:gfBlockSizeMinForZ[3] = {-32.0,-32.0,-4.0};
new Float:gfBlockSizeMaxForZ[3] = { 32.0, 32.0, 4.0};
new Float:gfDefaultBlockAngles[3] = { 0.0, 0.0, 0.0 };

/* Block models */
new const gszBlockModelDefault[] = "models/blockmaker/bm_block_default.mdl";
new const gszBlockModelPlatform[] = "models/blockmaker/bm_block_platform.mdl";
new const gszBlockModelBhop[] = "models/blockmaker/bm_block_bhop.mdl";
new const gszBlockModelDamage[] = "models/blockmaker/bm_block_damage.mdl";
new const gszBlockModelHealer[] = "models/blockmaker/bm_block_healer.mdl";
new const gszBlockModelInvincibility[] = "models/blockmaker/bm_block_invincibility.mdl";
new const gszBlockModelSpeedBoost[] = "models/blockmaker/bm_block_speedboost.mdl";
new const gszBlockModelNoFallDamage[] = "models/blockmaker/bm_block_nofalldamage.mdl";
new const gszBlockModelIce[] = "models/blockmaker/bm_block_ice.mdl";
new const gszBlockModelDeath[] = "models/blockmaker/bm_block_death.mdl";
new const gszBlockModelNuke[] = "models/blockmaker/bm_block_nuke.mdl";
new const gszBlockModelCamouflage[] = "models/blockmaker/bm_block_camouflage.mdl";
new const gszBlockModelLowGravity[] = "models/blockmaker/bm_block_lowgravity.mdl";
new const gszBlockModelFire[] = "models/blockmaker/bm_block_fire.mdl";
new const gszBlockModelRandom[] = "models/blockmaker/bm_block_random.mdl";
new const gszBlockModelSlap[] = "models/blockmaker/bm_block_slap.mdl";
new const gszBlockModelHoney[] = "models/blockmaker/bm_block_honey.mdl";
new const gszBlockModelBarrierCT[] = "models/blockmaker/bm_block_barrier_ct.mdl";
new const gszBlockModelBarrierT[] = "models/blockmaker/bm_block_barrier_t.mdl";
new const gszBlockModelBootsOfSpeed[] = "models/blockmaker/bm_block_bootsofspeed.mdl";

/* Block sprites */
new const gszBlockSpriteFire[] = "sprites/blockmaker/bm_block_fire.spr";			//custom
new const gszBlockSpriteTrampoline[] = "sprites/blockmaker/bm_block_trampoline.spr";	//custom
new const gszBlockSpriteSpeedBoost[] = "sprites/blockmaker/bm_block_speedboost.spr";	//custom
new const gszFireSprite[] = "sprites/blockmaker/bm_block_fire_flame.spr";		//custom

/* Block sounds */
new const gszNukeExplosion[] = "weapons/c4_explode1.wav";				//from CS
new const gszFireSoundFlame[] = "ambience/flameburst1.wav";				//from HL
new const gszInvincibleSound[] = "warcraft3/divineshield.wav";				//from WC3 plugin
new const gszCamouflageSound[] = "warcraft3/antend.wav";					//from WC3 plugin
new const gszStealthSound[] = "warcraft3/levelupcaster.wav";				//from WC3 plugin
new const gszBootsOfSpeedSound[] = "warcraft3/purgetarget1.wav";				//from WC3 plugin

/* Teleport */
new const Float:gfTeleportSizeMin[3] = {-16.0,-16.0,-16.0};
new const Float:gfTeleportSizeMax[3] = { 16.0, 16.0, 16.0};
new const Float:gfTeleportZOffset = 36.0;
new const gTeleportStartFrames = 20;
new const gTeleportEndFrames = 5;
new const gszTeleportSound[] = "warcraft3/blinkarrival.wav";				//from WC3 plugin
new const gszTeleportSpriteStart[] = "sprites/flare6.spr";				//from HL
new const gszTeleportSpriteEnd[] = "sprites/blockmaker/bm_teleport_end.spr";		//custom

/* Variables */
new gSpriteIdBeam;
new gSpriteIdFire;
new gMsgScreenFade;
new gMenuBeforeOptions[33];
new gChoiceOption[33];
new gBlockMenuPage[33];
new gTeleportStart[33];
new gGrabbed[33];
new gGroupedBlocks[33][256];
new gGroupCount[33];
new bool:gbSnapping[33];
new bool:gbNoFallDamage[33];
new bool:gbOnIce[33];
new bool:gbLowGravity[33];
new bool:gbOnFire[33];
new bool:gbJustDeleted[33];
new bool:gbAdminGodmode[33];
new bool:gbAdminNoclip[33];
new Float:gfSnappingGap[33];
new Float:gfOldMaxSpeed[33];
new Float:gfGrabOffset[33][3];
new Float:gfGrablength[33];
new Float:gfNextHealTime[33];
new Float:gfNextDamageTime[33];
new Float:gfInvincibleNextUse[33];
new Float:gfInvincibleTimeOut[33];
new Float:gfStealthNextUse[33];
new Float:gfStealthTimeOut[33];
new Float:gfTrampolineTimeout[33];
new Float:gfSpeedBoostTimeOut[33];
new Float:gfNukeNextUse[33];
new Float:gfCamouflageNextUse[33];
new Float:gfCamouflageTimeOut[33];
new Float:gfRandomNextUse[33];
new Float:gfBootsOfSpeedTimeOut[33];
new Float:gfBootsOfSpeedNextUse[33];
new gszCamouflageOldModel[33][32];

/* BLOCK & TELEPORT TYPES */
const gBlockMax = 21;
new gSelectedBlockType[gBlockMax];

new const gszBlockClassname[] = "bm_block";
new const gszSpriteClassname[] = "bm_sprite";
new const gszTeleportStartClassname[32] = "bm_teleportstart";
new const gszTeleportEndClassname[32] = "bm_teleportend";

enum
{
	TELEPORT_START,
	TELEPORT_END
};

enum
{
	BM_PLATFORM,		//A
	BM_BHOP,		//B
	BM_DAMAGE,		//C
	BM_HEALER,		//D
	BM_NOFALLDAMAGE,	//I
	BM_ICE,			//J
	BM_TRAMPOLINE,		//G
	BM_SPEEDBOOST,		//H
	BM_INVINCIBILITY,	//E
	BM_STEALTH,		//F
	BM_DEATH,		//K
	BM_NUKE,		//L
	BM_CAMOUFLAGE,		//M
	BM_LOWGRAVITY,		//N
	BM_FIRE,		//O
	BM_SLAP,		//P
	BM_RANDOM,		//Q
	BM_HONEY,		//R
	BM_BARRIER_CT,		//S
	BM_BARRIER_T,		//T
	BM_BOOTSOFSPEED		//U
};

new const gszBlockNames[gBlockMax][32] =
{
	"Platform",
	"Bunnyhop",
	"Damage",
	"Healer",
	"No Fall Damage",
	"Ice",
	"Trampoline",
	"Speed Boost",
	"Invincibility",
	"Stealth",
	"Death",
	"Nuke",
	"Camouflage",
	"Low Gravity",
	"Fire",
	"Slap",
	"Random",
	"Honey",
	"CT Barrier",
	"T Barrier",
	"Boots Of Speed"
};

new const gBlockSaveIds[gBlockMax] =
{
	'A', 'B', 'C', 'D', 'I', 'J', 'G', 'H', 'E', 'F', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U'
};

//array of blocks that the random block can be
const gRandomBlocksMax = 6;

new const gRandomBlocks[gRandomBlocksMax] =
{
	BM_INVINCIBILITY,
	BM_STEALTH,
	BM_DEATH,
	BM_CAMOUFLAGE,
	BM_SLAP,
	BM_BOOTSOFSPEED
};

//max speed for player when they have the boots of speed
const Float:gfBootsMaxSpeed = 400.0;

//how many pages for the block selection menu
new gBlockMenuPagesMax;

/***** PLUGIN START *****/
public plugin_init()
{
	register_plugin(PLUGIN, VERSION, AUTHOR);
	
	//register client commands
	register_clcmd("say /bm", "cmdShowMainMenu");
	register_clcmd("+bmgrab", "cmdGrab", BM_ADMIN_LEVEL, "bind a key to +bmgrab");
	register_clcmd("-bmgrab", "cmdRelease", BM_ADMIN_LEVEL);
	
	//register menus
	register_menucmd(register_menuid("bmMainMenu"), MAIN_MENU_KEYS, "handleMainMenu");
	register_menucmd(register_menuid("bmBlockSelectionMenu"), BLOCKSELECTION_MENU_KEYS, "handleBlockSelectionMenu");
	register_menucmd(register_menuid("bmOptionsMenu"), OPTIONS_MENU_KEYS, "handleOptionsMenu");
	register_menucmd(register_menuid("bmTeleportMenu"), TELEPORT_MENU_KEYS, "handleTeleportMenu");
	register_menucmd(register_menuid("bmChoiceMenu"), CHOICE_MENU_KEYS, "handleChoiceMenu");
	
	//register CVARs
	register_cvar("bm_telefrags", "1");			//players near teleport exit die if someone comes through
	register_cvar("bm_firedamageamount", "20.0");		//damage you take per half-second on the fire block
	register_cvar("bm_damageamount", "5.0");		//damage you take per half-second on the damage block
	register_cvar("bm_healamount", "1.0");			//how much hp per half-second you get on the healing block
	register_cvar("bm_invincibletime", "20.0");		//how long a player is invincible
	register_cvar("bm_invinciblecooldown", "60.0");		//time before the invincible block can be used again
	register_cvar("bm_stealthtime", "20.0");		//how long a player is in stealth
	register_cvar("bm_stealthcooldown", "60.0");		//time before the stealth block can be used again
	register_cvar("bm_camouflagetime", "20.0");		//how long a player is in camouflage
	register_cvar("bm_camouflagecooldown", "60.0");		//time before the camouflage block can be used again
	register_cvar("bm_nukecooldown", "60.0");		//someone might have been invincible when it was used
	register_cvar("bm_randomcooldown", "30.0");		//time before the random block can be used again
	register_cvar("bm_bootsofspeedtime", "20.0");		//how long the player has boots of speed
	register_cvar("bm_bootsofspeedcooldown", "60.0");	//time before boots of speed can be used again
	
	//register events
	register_event("DeathMsg", "eventPlayerDeath", "a");
	register_event("TextMsg", "eventRoundRestart", "a", "2&#Game_C", "2&#Game_w");
	register_event("ResetHUD", "eventPlayerSpawn", "b");
	register_event("CurWeapon", "eventCurWeapon", "be");
	
	//make config folder if it doesn't already exist
	new szConfigsDir[64];
	new szMap[32];
	get_configsdir(szConfigsDir, 64);
	add(szConfigsDir, 64, "/blockmaker");
	
	if (!dir_exists(szConfigsDir))
	{
		mkdir(szConfigsDir);
	}
	
	get_mapname(szMap, 32);
	formatex(gszFile, 96, "%s/%s.cfg", szConfigsDir, szMap);
	
	//make save folder in basedir (new saving/loading method)
	new szDir[64];
	get_basedir(szDir, 64);
	add(szDir, 64, "/blockmaker");
	
	if (!dir_exists(szDir))
	{
		mkdir(szDir);
	}
	
	get_mapname(szMap, 32);
	formatex(gszNewFile, 96, "%s/%s.bm", szDir, szMap);
}
 
public plugin_precache()
{
	//precache blocks
	precache_model(gszBlockModelDefault);
	precache_model(gszBlockModelDamage);
	precache_model(gszBlockModelIce);
	precache_model(gszBlockModelInvincibility);
	precache_model(gszBlockModelSpeedBoost);
	precache_model(gszBlockModelBhop);
	precache_model(gszBlockModelHealer);
	precache_model(gszBlockModelPlatform);
	precache_model(gszBlockModelNoFallDamage);
	precache_model(gszBlockModelDeath);
	precache_model(gszBlockModelNuke);
	precache_model(gszBlockModelCamouflage);
	precache_model(gszBlockModelLowGravity);
	precache_model(gszBlockModelFire);
	precache_model(gszBlockModelSlap);
	precache_model(gszBlockModelRandom);
	precache_model(gszBlockModelHoney);
	precache_model(gszBlockModelBarrierCT);
	precache_model(gszBlockModelBarrierT);
	precache_model(gszBlockModelBootsOfSpeed);
	
	//precache sprites
	precache_model(gszBlockSpriteFire);
	precache_model(gszBlockSpriteTrampoline);
	precache_model(gszBlockSpriteSpeedBoost);
	precache_model(gszTeleportSpriteStart);
	precache_model(gszTeleportSpriteEnd);
	gSpriteIdFire = precache_model(gszFireSprite);
	gSpriteIdBeam = precache_model("sprites/zbeam4.spr");
	
	//precache sounds
	precache_sound(gszTeleportSound);
	precache_sound(gszNukeExplosion);
	precache_sound(gszInvincibleSound);
	precache_sound(gszCamouflageSound);
	precache_sound(gszStealthSound);
	precache_sound(gszFireSoundFlame);
	precache_sound(gszBootsOfSpeedSound);
}

public plugin_cfg()
{
	//format help text filename
	format(gszHelpFilename, 32, gszHelpFilenameFormat, VERSION);
	
	//create main menu
	new size = sizeof(gszMenu);
	add(gszMenu, size, "\yBlock Maker Menu^n^n");
	add(gszMenu, size, "\r1. \wBlock Type: \y%s^n");
	add(gszMenu, size, "\r2. %sCreate Block^n");
	add(gszMenu, size, "\r3. %sConvert Block^n");
	add(gszMenu, size, "\r4. %sDelete Block^n");
	add(gszMenu, size, "\r5. %sRotate Block^n^n");
	add(gszMenu, size, "\r6. %sNoclip: %s^n");
	add(gszMenu, size, "\r7. %sGodmode: %s^n^n");
	add(gszMenu, size, "\r8. \wOptions Menu^n");
	add(gszMenu, size, "\r9. \wTeleport menu^n^n");
	add(gszMenu, size, "\r0. \wClose");
	
	//calculate maximum number of block menu pages from maximum amount of blocks
	gBlockMenuPagesMax = floatround((float(gBlockMax) / 8.0), floatround_ceil);
	
	//create the options menu
	size = sizeof(gszOptionsMenu);
	add(gszOptionsMenu, size, "\yOptions Menu^n^n");
	add(gszOptionsMenu, size, "\r1. %sSnapping: %s^n");
	add(gszOptionsMenu, size, "\r2. %sSnapping gap: \y%.1f^n");
	add(gszOptionsMenu, size, "\r3. %sAdd to group^n");
	add(gszOptionsMenu, size, "\r4. %sClear group^n^n");
	add(gszOptionsMenu, size, "\r5. %sDelete all blocks^n");
	add(gszOptionsMenu, size, "\r6. %sDelete all teleports^n^n");
	add(gszOptionsMenu, size, "\r7. %sSave to file^n");
	add(gszOptionsMenu, size, "\r8. %sLoad from file^n^n");
	add(gszOptionsMenu, size, "\r9. \wShow help^n");
	add(gszOptionsMenu, size, "\r0. \wBack");
	
	//create teleport menu
	size = sizeof(gszTeleportMenu);
	add(gszTeleportMenu, size, "\yTeleporter Menu^n^n");
	add(gszTeleportMenu, size, "\r1. %sTeleport Start^n");
	add(gszTeleportMenu, size, "\r2. %sTeleport Destination^n");
	add(gszTeleportMenu, size, "\r3. %sShow Teleport Path^n");
	add(gszTeleportMenu, size, "\r4. %sDelete Teleport^n^n^n");
	add(gszTeleportMenu, size, "\r6. %sNoclip: %s^n");
	add(gszTeleportMenu, size, "\r7. %sGodmode: %s^n^n");
	add(gszTeleportMenu, size, "\r8. \wOptions Menu^n^n^n");
	add(gszTeleportMenu, size, "\r0. \wBack");
	
	//create choice (YES/NO) menu
	size = sizeof(gszChoiceMenu);
	add(gszChoiceMenu, size, "\y%s^n^n");
	add(gszChoiceMenu, size, "\r1. \wYes^n");
	add(gszChoiceMenu, size, "\r2. \wNo^n^n^n^n^n^n^n^n^n^n^n");
	add(gszChoiceMenu, size, "\r0. \wBack");
	
	//create help title
	format(gszHelpTitle, sizeof(gszHelpTitle), "%s v%s by %s", PLUGIN, VERSION, AUTHOR);
	
	//read in help text from file
	new szConfigsDir[32];
	new szHelpFilename[64];
	new szLine[128];
	get_configsdir(szConfigsDir, 32);
	format(szHelpFilename, sizeof(szHelpFilename), "%s/%s", szConfigsDir, gszHelpFilename);
	
	//open help file for reading
	new f = fopen(szHelpFilename, "rt");
	
	//iterate through all the lines in the file
	size = sizeof(gszHelpText);
	while (!feof(f))
	{
		fgets(f, szLine, 128);
		
		add(gszHelpText, size, szLine);
	}
	
	//close file
	fclose(f);
	
	//get id for message 'ScreenFade'
	gMsgScreenFade = get_user_msgid("ScreenFade");
	
	//load blocks from file
	loadBlocks(0);
}

/***** FORWARDS *****/
public client_connect(id)
{
	//make sure snapping is on by default
	gbSnapping[id] = true;
	
	//players chosen snapping gap defaults to 0.0 units
	gfSnappingGap[id] = 0.0;
	
	//make sure players can die
	gbNoFallDamage[id] = false;
	
	//players block selection menu is on page 1
	gBlockMenuPage[id] = 1;
	
	//player doesn't have godmode or noclip
	gbAdminGodmode[id] = false;
	gbAdminNoclip[id] = false;
	
	//player doesn't have any block grouped
	gGroupCount[id] = 0;
}

public client_disconnect(id)
{
	//clear players group
	groupClear(id);
	
	//if player was grabbing an entity when they disconnected
	if (gGrabbed[id])
	{
		//if entity is valid
		if (is_valid_ent(gGrabbed[id]))
		{
			//set the entity to 'not being grabbed'
			entity_set_int(gGrabbed[id], EV_INT_iuser2, 0);
		}
		
		gGrabbed[id] = 0;
	}
}

public pfn_touch(ent, id)
{
	//if touch event involves a player
	if (id > 0 && id <= 32)
	{
		//if player is alive
		if (is_user_alive(id))
		{
			//if entity involved is a block
			if (isBlock(ent))
			{
				//get the blocktype
				new blockType = entity_get_int(ent, EV_INT_body);
				
				//if blocktype is a bunnyhop block or barrier
				if (blockType == BM_BHOP || blockType == BM_BARRIER_CT || blockType == BM_BARRIER_T)
				{
					//if task does not already exist for bunnyhop block
					if (!task_exists(TASK_BHOPSOLIDNOT + ent) && !task_exists(TASK_BHOPSOLID + ent))
					{
						//get the players team
						new CsTeams:team = cs_get_user_team(id);
						
						//if players team is different to barrier
						if (blockType == BM_BARRIER_CT && team == CS_TEAM_T)
						{
							//make block SOLID_NOT without any delay
							taskSolidNot(TASK_BHOPSOLIDNOT + ent);
						}
						else if (blockType == BM_BARRIER_T && team == CS_TEAM_CT)
						{
							//make block SOLID_NOT without any delay
							taskSolidNot(TASK_BHOPSOLIDNOT + ent);
						}
						else if (blockType == BM_BHOP)
						{
							//set bhop block to be SOLID_NOT after 0.1 seconds
							set_task(0.1, "taskSolidNot", TASK_BHOPSOLIDNOT + ent);
						}
					}
				}
			}
		}
	}
	
	return PLUGIN_CONTINUE;
}

public server_frame()
{
	new ent;
	new Float:vOrigin[3];
	new bool:entNear = false;
	new tele;
	
	//iterate through all players and remove slow down after jumping for any 'on ice'
	for (new i = 1; i <= 32; ++i)
	{
		if (is_user_alive(i) && gbOnIce[i])
		{
			entity_set_float(i, EV_FL_fuser2, 0.0);
		}
	}
	
	//find all teleport start entities in map and if a player is close to one, teleport the player
	while ((ent = find_ent_by_class(ent, gszTeleportStartClassname)))
	{
		new Float:vOrigin[3];
		entity_get_vector(ent, EV_VEC_origin, vOrigin);
		
		//teleport players and grenades within a sphere around the teleport start entity
		new entinsphere = -1;
		while ((entinsphere = find_ent_in_sphere(entinsphere, vOrigin, 32.0)))
		{
			//get classname of entity
			new szClassname[32];
			entity_get_string(entinsphere, EV_SZ_classname, szClassname, 32);
			
			//if entity is a player
			if (entinsphere > 0 && entinsphere <= 32)
			{
				//only teleport player if they're alive
				if (is_user_alive(entinsphere))
				{
					//teleport the player
					actionTeleport(entinsphere, ent);
				}
			}
			//or if entity is a grenade
			else if (equal(szClassname, "grenade"))
			{
				//get the end of the teleport
				tele = entity_get_int(ent, EV_INT_iuser1);
				
				//if the end of the teleport exists
				if (tele)
				{
					//set the end of the teleport to be not solid
					entity_set_int(tele, EV_INT_solid, SOLID_NOT);	//can't be grabbed or deleted
					
					//teleport the grenade
					actionTeleport(entinsphere, ent);
					
					//set a time in the teleport it will become solid after 2 seconds
					entity_set_float(tele, EV_FL_ltime, halflife_time() + 2.0);
				}
			}
		}
	}
	
	//make teleporters SOLID_NOT when players are near them
	while ((ent = find_ent_by_class(ent, gszTeleportEndClassname)))
	{
		//get the origin of the teleport end entity
		entity_get_vector(ent, EV_VEC_origin, vOrigin);
		
		//compare this origin with all player and grenade origins
		new entinsphere = -1;
		while ((entinsphere = find_ent_in_sphere(entinsphere, vOrigin, 64.0)))
		{
			//get classname of entity
			new szClassname[32];
			entity_get_string(entinsphere, EV_SZ_classname, szClassname, 32);
			
			//if entity is a player
			if (entinsphere > 0 && entinsphere <= 32)
			{
				//make sure player is alive
				if (is_user_alive(entinsphere))
				{
					entNear = true;
					
					break;
				}
			}
			//or if entity is a grenade
			else if (equal(szClassname, "grenade"))
			{
				entNear = true;
				
				break;
			}
		}
		
		//set the solid type of the teleport end entity depending on whether or not a player is near
		if (entNear)
		{
			//only do this if it is not being grabbed
			if (entity_get_int(ent, EV_INT_iuser2) == 0)
			{
				entity_set_int(ent, EV_INT_solid, SOLID_NOT);	//can't be grabbed or deleted
			}
		}
		else
		{
			//get time from teleport end entity to check if it can go solid
			new Float:fTime = entity_get_float(ent, EV_FL_ltime);
			
			//only set teleport end entity to solid if its 'solid not' time has elapsed
			if (halflife_time() >= fTime)
			{
				entity_set_int(ent, EV_INT_solid, SOLID_BBOX);	//CAN be grabbed and deleted
			}
		}
	}
	
	//find all block entities
	while ((ent = find_ent_by_class(ent, gszBlockClassname)))
	{
		//get block type
		new blockType = entity_get_int(ent, EV_INT_body);
		
		//if block is a speed boost
		if (blockType == BM_SPEEDBOOST)
		{
			new Float:vOrigin[3];
			new Float:pOrigin[3];
			new Float:dist = 9999.9;
			new Float:playerDist = 9999.9;
			new nearestPlayer = 0;
			
			//get the origin of the speed boost block
			entity_get_vector(ent, EV_VEC_origin, vOrigin);
			
			//compare this origin with all players origins to find the nearest player to the block
			for (new id = 1; id <= 32; ++id)
			{
				//if player is alive
				if (is_user_alive(id))
				{
					//get player origin
					entity_get_vector(id, EV_VEC_origin, pOrigin);
					
					//get the distance from the block to the player
					dist = get_distance_f(vOrigin, pOrigin);
					
					if (dist < playerDist)
					{
						nearestPlayer = id;
						playerDist = dist;
					}
				}
			}
			
			//if we found a player within 100 units of the speed boost block
			if (nearestPlayer > 0 && playerDist < 200.0)
			{
				//get the sprite on top of the speed boost block
				new sprite = entity_get_int(ent, EV_INT_iuser3);
				
				//make sure sprite entity is valid
				if (sprite)
				{
					new Float:vAngles[3];
					
					//get the direction the player is looking
					entity_get_vector(nearestPlayer, EV_VEC_angles, vAngles);
					
					//set the angles of the sprite to be the same as the player
					vAngles[0] = 90.0;	//always make sure sprite is flat to the block
					vAngles[1] += 90.0;	//rotate the sprite by 90 because it doesnt point up (PAT!)
					entity_set_vector(sprite, EV_VEC_angles, vAngles);
				}
			}
		}
	}
}

public client_PreThink(id)
{
	//make sure player is connected
	if (is_user_connected(id))
	{
		//display type of block that player is aiming at
		new ent, body;
		get_user_aiming(id, ent, body, 320);
		
		if (isBlock(ent))
		{
			new blockType = entity_get_int(ent, EV_INT_body);
			
			set_hudmessage(gHudRed, gHudGreen, gHudBlue, gfTextX, gfTextY, gHudEffects, gfHudFxTime, gfHudHoldTime, gfHudFadeInTime, gfHudFadeOutTime, gHudChannel);
			show_hudmessage(id, "Block Type: %s", gszBlockNames[blockType]);
		}
		
		//make sure player is alive
		if (is_user_alive(id))
		{
			//if player has low gravity
			if (gbLowGravity[id])
			{
				//get players flags
				new flags = entity_get_int(id, EV_INT_flags);
				
				//if player has feet on the ground, set gravity to normal
				if (flags & FL_ONGROUND)
				{
					set_user_gravity(id);
					
					gbLowGravity[id] = false;
				}
			}
			
			//trace directly down to see if there is a block beneath player
			new Float:pOrigin[3];
			new Float:pSize[3];
			new Float:pMaxs[3];
			new Float:vTrace[3];
			new Float:vReturn[3];
			entity_get_vector(id, EV_VEC_origin, pOrigin);
			entity_get_vector(id, EV_VEC_size, pSize);
			entity_get_vector(id, EV_VEC_maxs, pMaxs);
			
			//calculate position of players feet
			pOrigin[2] = pOrigin[2] - ((pSize[2] - 36.0) - (pMaxs[2] - 36.0));
			
			//make the trace small for some blocks
			vTrace[2] = pOrigin[2] - 1.0;
			
			//do 4 traces for each corner of the player
			for (new i = 0; i < 4; ++i)
			{
				switch (i)
				{
					case 0: { vTrace[0] = pOrigin[0] - 16; vTrace[1] = pOrigin[1] + 16; }
					case 1: { vTrace[0] = pOrigin[0] + 16; vTrace[1] = pOrigin[1] + 16; }
					case 2: { vTrace[0] = pOrigin[0] + 16; vTrace[1] = pOrigin[1] - 16; }
					case 3: { vTrace[0] = pOrigin[0] - 16; vTrace[1] = pOrigin[1] - 16; }
				}
				
				ent = trace_line(id, pOrigin, vTrace, vReturn);
				
				//if entity found is a block
				if (isBlock(ent))
				{
					new blockType = entity_get_int(ent, EV_INT_body);
					
					switch (blockType)
					{
						case BM_HEALER: actionHeal(id);
						case BM_DAMAGE: actionDamage(id);
						case BM_INVINCIBILITY: actionInvincible(id, false);
						case BM_STEALTH: actionStealth(id, false);
						case BM_TRAMPOLINE: actionTrampoline(id);
						case BM_SPEEDBOOST: actionSpeedBoost(id);
						case BM_DEATH: actionDeath(id);
						case BM_NUKE: actionNuke(id, false);
						case BM_LOWGRAVITY: actionLowGravity(id);
						case BM_CAMOUFLAGE: actionCamouflage(id, false);
						case BM_FIRE: actionFire(id, ent);
						case BM_SLAP: actionSlap(id);
						case BM_RANDOM: actionRandom(id, ent);
						case BM_HONEY: actionHoney(id);
						case BM_BOOTSOFSPEED: actionBootsOfSpeed(id, false);
					}
				}
			}
			
			//make the trace longer for other blocks
			vTrace[2] = pOrigin[2] - 20.0;
			
			//do 4 traces for each corner of the player
			for (new i = 0; i < 4; ++i)
			{
				switch (i)
				{
					case 0: { vTrace[0] = pOrigin[0] - 16; vTrace[1] = pOrigin[1] + 16; }
					case 1: { vTrace[0] = pOrigin[0] + 16; vTrace[1] = pOrigin[1] + 16; }
					case 2: { vTrace[0] = pOrigin[0] + 16; vTrace[1] = pOrigin[1] - 16; }
					case 3: { vTrace[0] = pOrigin[0] - 16; vTrace[1] = pOrigin[1] - 16; }
				}
				
				ent = trace_line(id, pOrigin, vTrace, vReturn);
				
				//if entity found is a block
				if (isBlock(ent))
				{
					new blockType = entity_get_int(ent, EV_INT_body);
					
					switch (blockType)
					{
						case BM_TRAMPOLINE: actionTrampoline(id);
						case BM_NOFALLDAMAGE: actionNoFallDamage(id);
						case BM_ICE: actionOnIce(id);
					}
				}
			}
			
			//display amount of invincibility/stealth/camouflage/boots of speed timeleft
			new Float:fTime = halflife_time();
			new Float:fTimeleftInvincible = gfInvincibleTimeOut[id] - fTime;
			new Float:fTimeleftStealth = gfStealthTimeOut[id] - fTime;
			new Float:fTimeleftCamouflage = gfCamouflageTimeOut[id] - fTime;
			new Float:fTimeleftBootsOfSpeed = gfBootsOfSpeedTimeOut[id] - fTime;
			new szTextToShow[256] = "";
			new szText[48];
			new bool:bShowText = false;
			
			if (fTimeleftInvincible >= 0.0)
			{
				format(szText, sizeof(szText), "Invincible: %.1f^n", fTimeleftInvincible);
				add(szTextToShow, sizeof(szTextToShow), szText);
				bShowText = true;
			}
			
			if (fTimeleftStealth >= 0.0)
			{
				format(szText, sizeof(szText), "Stealth: %.1f^n", fTimeleftStealth);
				add(szTextToShow, sizeof(szTextToShow), szText);
				bShowText = true;
			}
			
			if (fTimeleftCamouflage >= 0.0)
			{
				//if player is a CT
				if (get_user_team(id) == 1)
				{
					format(szText, sizeof(szText), "You look like a Counter-Terrorist: %.1f^n", fTimeleftCamouflage);
				}
				else
				{
					format(szText, sizeof(szText), "You look like a Terrorist: %.1f^n", fTimeleftCamouflage);
				}
				
				add(szTextToShow, sizeof(szTextToShow), szText);
				bShowText = true;
			}
			
			if (fTimeleftBootsOfSpeed >= 0.0)
			{
				format(szText, sizeof(szText), "Boots of speed: %.1f^n", fTimeleftBootsOfSpeed);
				add(szTextToShow, sizeof(szTextToShow), szText);
				bShowText = true;
			}
			
			//if there is some text to show then show it
			if (bShowText)
			{
				set_hudmessage(gHudRed, gHudGreen, gHudBlue, gfTextX, gfTextY, gHudEffects, gfHudFxTime, gfHudHoldTime, gfHudFadeInTime, gfHudFadeOutTime, gHudChannel);
				show_hudmessage(id, szTextToShow);
			}
		}
		
		//get players buttons
		new buttons = get_user_button(id);
		new oldbuttons = get_user_oldbutton(id);
		
		//if player has grabbed an entity
		if (gGrabbed[id] > 0)
		{
			//check for a single press on the following buttons
			if (buttons & IN_JUMP && !(oldbuttons & IN_JUMP)) cmdJump(id);
			if (buttons & IN_DUCK && !(oldbuttons & IN_DUCK)) cmdDuck(id);
			if (buttons & IN_ATTACK && !(oldbuttons & IN_ATTACK)) cmdAttack(id);
			if (buttons & IN_ATTACK2 && !(oldbuttons & IN_ATTACK2)) cmdAttack2(id);
			
			//prevent player from using attack
			buttons &= ~IN_ATTACK;
			entity_set_int(id, EV_INT_button, buttons);
			
			//if player has grabbed a valid entity
			if (is_valid_ent(gGrabbed[id]))
			{
				//if block the player is grabbing is in their group and group count is larger than 1
				if (isBlockInGroup(id, gGrabbed[id]) && gGroupCount[id] > 1)
				{
					new Float:vMoveTo[3];
					new Float:vOffset[3];
					new Float:vOrigin[3];
					new block;
					
					//move the block the player has grabbed and get the move vector
					moveGrabbedEntity(id, vMoveTo);
					
					//move the rest of the blocks in the players group using vector for grabbed block
					for (new i = 0; i <= gGroupCount[id]; ++i)
					{
						block = gGroupedBlocks[id][i];
						
						//if block is still in this players group
						if (isBlockInGroup(id, block))
						{
							//get offset vector from block
							entity_get_vector(block, EV_VEC_vuser1, vOffset);
							
							vOrigin[0] = vMoveTo[0] - vOffset[0];
							vOrigin[1] = vMoveTo[1] - vOffset[1];
							vOrigin[2] = vMoveTo[2] - vOffset[2];
							
							//move grouped block
							moveEntity(id, block, vOrigin, false);
						}
					}
				}
				else
				{
					//move the entity the player has grabbed
					moveGrabbedEntity(id);
				}
			}
			else
			{
				cmdRelease(id);
			}
		}
		
		//if player has just deleted something
		if (gbJustDeleted[id])
		{
			//if player is pressing attack2
			if (buttons & IN_ATTACK2)
			{
				//prevent player from using attack2
				buttons &= ~IN_ATTACK2;
				entity_set_int(id, EV_INT_button, buttons);
			}
			else
			{
				//player has now NOT just deleted something
				gbJustDeleted[id] = false;
			}
		}
	}
	
	return PLUGIN_CONTINUE;
}

public client_PostThink(id)
{
	//if player is aliev
	if (is_user_alive(id))
	{
		//if player is set no not get fall damage
		if (gbNoFallDamage[id])
		{
			entity_set_int(id,  EV_INT_watertype, -3);
			gbNoFallDamage[id] = false;
		}
	}
}

/***** EVENTS *****/
public eventPlayerDeath()
{
	new id = read_data(2);
	
	resetTimers(id);
}

public eventRoundRestart()
{
	//iterate through all players
	for (new id = 1; id <= 32; ++id)
	{
		//reset all players timers
		resetTimers(id);
	}
}

public eventPlayerSpawn(id)
{
	//if player has godmode enabled
	if (gbAdminGodmode[id])
	{
		//re-enable godmode on player
		set_user_godmode(id, 1);
	}
	
	//if player has noclip enabled
	if (gbAdminNoclip[id])
	{
		//re-enable noclip on player
		set_user_noclip(id, 1);
	}
}

resetTimers(id)
{
	gfInvincibleTimeOut[id] = 0.0;
	gfInvincibleNextUse[id] = 0.0;
	gfStealthTimeOut[id] = 0.0;
	gfStealthNextUse[id] = 0.0;
	gfCamouflageTimeOut[id] = 0.0;
	gfCamouflageNextUse[id] = 0.0;
	gfNukeNextUse[id] = 0.0;
	gbOnFire[id] = false;
	gfRandomNextUse[id] = 0.0;
	gfBootsOfSpeedTimeOut[id] = 0.0;
	gfBootsOfSpeedNextUse[id] = 0.0;
	
	//remove any task this player might have
	new taskId = TASK_INVINCIBLE + id;
	if (task_exists(taskId)) 
	{
		remove_task(taskId);
	}
	
	taskId = TASK_STEALTH + id;
	if (task_exists(taskId))
	{
		remove_task(taskId);
	}
	
	taskId = TASK_CAMOUFLAGE + id;
	if (task_exists(taskId))
	{
		remove_task(taskId);
		
		//change back to players old model
		cs_set_user_model(id, gszCamouflageOldModel[id]);
	}
	
	//make sure player is connected
	if (is_user_connected(id))
	{
		//set players rendering to normal
		set_user_rendering(id, kRenderFxGlowShell, 0, 0, 0, kRenderNormal, 255);
	}
}

public eventCurWeapon(id)
{
	new Float:fTime = halflife_time();
	new Float:fTimeleftBootsOfSpeed = gfBootsOfSpeedTimeOut[id] - fTime;
	
	//if the player has the boots of speed
	if (fTimeleftBootsOfSpeed >= 0.0)
	{
		//set their max speed so they obtain their speed after changing weapon
		set_user_maxspeed(id, gfBootsMaxSpeed);
	}
}

/***** BLOCK ACTIONS *****/
actionDamage(id)
{
	if (halflife_time() >= gfNextDamageTime[id])
	{
		if (get_user_health(id) > 0)
		{
			new Float:amount = get_cvar_float("bm_damageamount");
			fakedamage(id, "damage block", amount, DMG_CRUSH);
		}
		
		gfNextDamageTime[id] = halflife_time() + 0.5;
	}
}

actionHeal(id)
{
	if (halflife_time() >= gfNextHealTime[id])
	{
		new hp = get_user_health(id);
		new amount = floatround(get_cvar_float("bm_healamount"), floatround_floor);
		new sum = (hp + amount);
		
		if (sum < 100)
		{
			set_user_health(id, sum);
		}
		else
		{
			set_user_health(id, 100);
		}
		
		gfNextHealTime[id] = halflife_time() + 0.5;
	}
}

actionInvincible(id, OverrideTimer)
{
	new Float:fTime = halflife_time();
	
	if (fTime >= gfInvincibleNextUse[id] || OverrideTimer)
	{
		new Float:fTimeout = get_cvar_float("bm_invincibletime");
		
		set_user_godmode(id, 1);
		set_task(fTimeout, "taskInvincibleRemove", TASK_INVINCIBLE + id, "", 0, "a", 1);
		
		//only make player glow white for invincibility if player isn't already stealth
		if (fTime >= gfStealthTimeOut[id])
		{
			set_user_rendering(id, kRenderFxGlowShell, 255, 255, 255, kRenderNormal, 16);
		}
		
		//play invincibility sound
		emit_sound(id, CHAN_STATIC, gszInvincibleSound, 1.0, ATTN_NORM, 0, PITCH_NORM);
		
		gfInvincibleTimeOut[id] = fTime + fTimeout;
		gfInvincibleNextUse[id] = fTime + fTimeout + get_cvar_float("bm_invinciblecooldown");
	}
	else
	{
		set_hudmessage(gHudRed, gHudGreen, gHudBlue, gfTextX, gfTextY, gHudEffects, gfHudFxTime, gfHudHoldTime, gfHudFadeInTime, gfHudFadeOutTime, gHudChannel);
		show_hudmessage(id, "Invincibility next use: %.1f", gfInvincibleNextUse[id] - fTime);
	}
}

actionStealth(id, OverrideTimer)
{
	new Float:fTime = halflife_time();
	
	//check if player is outside of cooldown time to use stealth
	if (fTime >= gfStealthNextUse[id] || OverrideTimer)
	{
		new Float:fTimeout = get_cvar_float("bm_stealthtime");
		
		//set a task to remove stealth after time out amount
		set_task(fTimeout, "taskStealthRemove", TASK_STEALTH + id, "", 0, "a", 1);
		
		//make player invisible
		set_user_rendering(id, kRenderFxGlowShell, 0, 0, 0, kRenderTransColor, 0);
		
		//play stealth sound
		emit_sound(id, CHAN_STATIC, gszStealthSound, 1.0, ATTN_NORM, 0, PITCH_NORM);
		
		gfStealthTimeOut[id] = fTime + fTimeout;
		gfStealthNextUse[id] = fTime + fTimeout + get_cvar_float("bm_stealthcooldown");
	}
	else
	{
		set_hudmessage(gHudRed, gHudGreen, gHudBlue, gfTextX, gfTextY, gHudEffects, gfHudFxTime, gfHudHoldTime, gfHudFadeInTime, gfHudFadeOutTime, gHudChannel);
		show_hudmessage(id, "Stealth next use: %.1f", gfStealthNextUse[id] - fTime);
	}
}

actionTrampoline(id)
{
	//if trampoline timeout has exceeded (needed to prevent velocity being given multiple times)
	if (halflife_time() >= gfTrampolineTimeout[id])
	{
		new Float:velocity[3];
		
		//set player Z velocity to make player bounce
		entity_get_vector(id, EV_VEC_velocity, velocity);
		velocity[2] = 500.0;					//jump velocity
		entity_set_vector(id, EV_VEC_velocity, velocity);
		
		entity_set_int(id, EV_INT_gaitsequence, 6);   		//play the Jump Animation
		
		gfTrampolineTimeout[id] = halflife_time() + 0.5;
	}
}

actionSpeedBoost(id)
{
	//if speed boost timeout has exceeded (needed to prevent speed boost being given multiple times)
	if (halflife_time() >= gfSpeedBoostTimeOut[id])
	{
		new Float:pAim[3];
		
		//set velocity on player in direction they're aiming
		velocity_by_aim(id, 800, pAim);
		pAim[2] = 260.0;					//make sure Z velocity is only as high as a jump
		entity_set_vector(id, EV_VEC_velocity, pAim);
		
		entity_set_int(id, EV_INT_gaitsequence, 6);   		//play the Jump Animation
		
		gfSpeedBoostTimeOut[id] = halflife_time() + 0.5;
	}
}

actionNoFallDamage(id)
{
	//set the player to not receive any fall damage (handled in client_PostThink)
	gbNoFallDamage[id] = true;
}

actionOnIce(id)
{
	new taskid = TASK_ICE + id;
	
	if (!gbOnIce[id])
	{
		//save players maxspeed value
		gfOldMaxSpeed[id] = get_user_maxspeed(id);
		
		//make player feel like they're on ice
		entity_set_float(id, EV_FL_friction, 0.15);
		set_user_maxspeed(id, 600.0);
		
		//player is now 'on ice'
		gbOnIce[id] = true;
	}
	
	//remove any existing 'not on ice' task
	if (task_exists(taskid))
	{
		remove_task(taskid);
	}
	
	//set task to remove 'on ice' effect very soon (task replaced if player is still on ice before task time reached)
	set_task(0.1, "taskNotOnIce", taskid);
}

actionDeath(id)
{
	//if player does not have godmode enabled (admin godmode or invincibility)
	if (!get_user_godmode(id))
	{
		//kill player by inflicting massive damage
		fakedamage(id, "the block of death", 10000.0, DMG_GENERIC);
	}
}

actionNuke(id, OverrideTimer)
{
	//get game time
	new Float:fTime = halflife_time();
	
	//make sure player is alive
	if (is_user_alive(id) && (fTime >= gfNukeNextUse[id] || OverrideTimer))
	{
		//get players team
		new CsTeams:playerTeam = cs_get_user_team(id);
		new CsTeams:team;
		
		//iterate through all players
		for (new i = 1; i <= 32; ++i)
		{
			//make sure player is alive
			if (is_user_alive(i))
			{
				team = cs_get_user_team(i);
				
				//if this player is on a different team to the player that used the nuke
				if ((team == CS_TEAM_T && playerTeam == CS_TEAM_CT) || (team == CS_TEAM_CT && playerTeam == CS_TEAM_T))
				{
					//slay player
					fakedamage(i, "a nuke", 10000.0, DMG_BLAST);
				}
			}
			
			//make sure player is connected
			if (is_user_connected(i))
			{
				//make the screen flash for a nuke effect
				message_begin(MSG_ONE, gMsgScreenFade, {0, 0, 0}, i);
				write_short(1024);	//duration
				write_short(1024);	//hold time
				write_short(4096);	//type (in / out)
				write_byte(255);	//red
				write_byte(255);	//green
				write_byte(255);	//blue
				write_byte(255);	//alpha
				message_end();
			}
		}
		
		//play explosion sound
		emit_sound(0, CHAN_STATIC, gszNukeExplosion, 1.0, ATTN_NORM, 0, PITCH_NORM);
		
		//set the time when the player can use the nuke again (someone might have been invincible)
		gfNukeNextUse[id] = fTime + get_cvar_float("bm_nukecooldown");
		
		//get the name of the player that used the nuke
		new szPlayerName[32];
		get_user_name(id, szPlayerName, 32);
		
		//setup hud message to show who nuked what team
		set_hudmessage(255, 255, 0, -1.0, 0.35, 0, 6.0, 10.0, 1.0, 1.0);
		
		//show message saying player nuked the other team
		if (playerTeam == CS_TEAM_T)
		{
			show_hudmessage(0, "%s just nuked the Counter-Terrorists", szPlayerName);
		}
		else
		{
			show_hudmessage(0, "%s just nuked the Terrorists", szPlayerName);
		}
	}
	else
	{
		set_hudmessage(gHudRed, gHudGreen, gHudBlue, gfTextX, gfTextY, gHudEffects, gfHudFxTime, gfHudHoldTime, gfHudFadeInTime, gfHudFadeOutTime, gHudChannel);
		show_hudmessage(id, "Nuke next use: %.1f", gfNukeNextUse[id] - fTime);
	}
}

actionCamouflage(id, OverrideTimer)
{
	new Float:fTime = halflife_time();
	
	//check if player is outside of cooldown time to use camouflage
	if (fTime >= gfCamouflageNextUse[id] || OverrideTimer)
	{
		new Float:fTimeout = get_cvar_float("bm_camouflagetime");
		
		//get players team and model
		new szModel[32];
		new team;
		
		cs_get_user_model(id, szModel, 32);
		
		team = get_user_team(id);
		
		//save current player model
		gszCamouflageOldModel[id] = szModel;
		
		//change player model depending on their current team
		if (team == 1)		//TERRORIST
		{
			cs_set_user_model(id, "urban");
		}
		else
		{
			cs_set_user_model(id, "leet");
		}
		
		//play camouflage sound
		emit_sound(id, CHAN_STATIC, gszCamouflageSound, 1.0, ATTN_NORM, 0, PITCH_NORM);
		
		//set a task to remove camouflage after time out amount
		set_task(fTimeout, "taskCamouflageRemove", TASK_CAMOUFLAGE + id, "", 0, "a", 1);
		
		//set timers to prevent player from using camouflage again so soon
		gfCamouflageTimeOut[id] = fTime + fTimeout;
		gfCamouflageNextUse[id] = fTime + fTimeout + get_cvar_float("bm_camouflagecooldown");
	}
	else
	{
		set_hudmessage(gHudRed, gHudGreen, gHudBlue, gfTextX, gfTextY, gHudEffects, gfHudFxTime, gfHudHoldTime, gfHudFadeInTime, gfHudFadeOutTime, gHudChannel);
		show_hudmessage(id, "Camouflage next use: %.1f", gfCamouflageNextUse[id] - fTime);
	}
}

actionLowGravity(id)
{
	//set player to have low gravity
	set_user_gravity(id, 0.25);
	
	//set global boolean showing player has low gravity
	gbLowGravity[id] = true;
}

actionFire(id, ent)
{
	if (halflife_time() >= gfNextDamageTime[id])
	{
		new hp = get_user_health(id);
		
		//if players health is greater than 0
		if (hp > 0)
		{
			//if player does not have godmode
			if (!get_user_godmode(id))
			{
				new Float:amount = get_cvar_float("bm_firedamageamount") / 10.0;
				new Float:newAmount = hp - amount;
				
				//if this amount of damage won't kill the player
				if (newAmount > 0)
				{
					set_user_health(id, floatround(newAmount, floatround_floor));
				}
				else
				{
					//use fakedamage to kill the player
					fakedamage(id, "fire block", amount, DMG_BURN);
				}
			}
			
			//get halflife time and time for next fire sound from fire block
			new Float:fTime = halflife_time();
			new Float:fNextFireSoundTime = entity_get_float(ent, EV_FL_ltime); 
			
			//if the current time is greater than or equal to the next time to play the fire sound
			if (fTime >= fNextFireSoundTime)
			{
				//play the fire sound
				emit_sound(ent, CHAN_ITEM, gszFireSoundFlame, 0.6, ATTN_NORM, 0, PITCH_NORM);
				
				//set the fire blocks time
				entity_set_float(ent, EV_FL_ltime, fTime + 0.75);
			}
			
			//get effects vectors using block origin
			new Float:origin1[3];
			new Float:origin2[3];
			entity_get_vector(ent, EV_VEC_origin, origin1);
			entity_get_vector(ent, EV_VEC_origin, origin2);
			origin1[0] -= 32.0;
			origin1[1] -= 32.0;
			origin1[2] += 10.0;
			origin2[0] += 32.0;
			origin2[1] += 32.0;
			origin2[2] += 10.0;
			
			//get a random height for the flame
			new randHeight = random_num(0, 32) + 16;
			
			//show some effects
			message_begin(MSG_BROADCAST, SVC_TEMPENTITY);
			write_byte(TE_BUBBLES);
			write_coord(floatround(origin1[0], floatround_floor));	//min start position
			write_coord(floatround(origin1[1], floatround_floor));
			write_coord(floatround(origin1[2], floatround_floor));
			write_coord(floatround(origin2[0], floatround_floor));	//max start position
			write_coord(floatround(origin2[1], floatround_floor));
			write_coord(floatround(origin2[2], floatround_floor));
			write_coord(randHeight);				//float height
			write_short(gSpriteIdFire);				//model index
			write_byte(10);						//count
			write_coord(1);						//speed
			message_end();
		}
		
		gfNextDamageTime[id] = halflife_time() + 0.05;
	}
}

actionSlap(id)
{
	user_slap(id, 0);
	user_slap(id, 0);
	set_hudmessage(255, 255, 255, -1.0, 0.20, 0, 6.0, 12.0, 0.0, 1.0, 3);
	
	show_hudmessage(id, "GET OFF MY FACE!!!");
}

actionRandom(id, ent)
{
	new Float:fTime = halflife_time();
	
	//check if player is outside of cooldown time to use camouflage
	if (fTime >= gfRandomNextUse[id])
	{
		//get which type of block this is set to be
		new blockType = entity_get_int(ent, EV_INT_iuser4);
		
		//do the random block action
		switch (blockType)
		{
			case BM_INVINCIBILITY: actionInvincible(id, true);
			case BM_STEALTH: actionStealth(id, true);
			case BM_DEATH: actionDeath(id);
			case BM_CAMOUFLAGE: actionCamouflage(id, true);
			case BM_SLAP: actionSlap(id); 
			case BM_BOOTSOFSPEED: actionBootsOfSpeed(id, true);
		}
		
		//set timer to prevent player from using the random block again so soon
		gfRandomNextUse[id] = fTime + get_cvar_float("bm_randomcooldown");
		
		//set this random block to another random block!
		new randNum = random_num(0, gRandomBlocksMax - 1);
		entity_set_int(ent, EV_INT_iuser4, gRandomBlocks[randNum]);
	}
	else
	{
		set_hudmessage(gHudRed, gHudGreen, gHudBlue, gfTextX, gfTextY, gHudEffects, gfHudFxTime, gfHudHoldTime, gfHudFadeInTime, gfHudFadeOutTime, gHudChannel);
		show_hudmessage(id, "Random block next use: %.1f", gfRandomNextUse[id] - fTime);
	}
}

actionHoney(id)
{
	new taskid = TASK_HONEY + id;
	
	//make player feel like they're stuck in honey by lowering their maxspeed
	set_user_maxspeed(id, 50.0);
	
	//remove any existing 'in honey' task
	if (task_exists(taskid))
	{
		remove_task(taskid);
	}
	else
	{
		//half the players velocity the first time they touch it
		new Float:vVelocity[3];
		entity_get_vector(id, EV_VEC_velocity, vVelocity);
		vVelocity[0] = vVelocity[0] / 2.0;
		vVelocity[1] = vVelocity[1] / 2.0;
		entity_set_vector(id, EV_VEC_velocity, vVelocity);
	}
	
	//set task to remove 'in honey' effect very soon (task replaced if player is still in honey before task time reached)
	set_task(0.1, "taskNotInHoney", taskid);
}

actionBootsOfSpeed(id, bool:OverrideTimer)
{
	new Float:fTime = halflife_time();
	
	//check if player is outside of cooldown time to use the boots of speed
	if (fTime >= gfBootsOfSpeedNextUse[id] || OverrideTimer)
	{
		new Float:fTimeout = get_cvar_float("bm_bootsofspeedtime");
		
		//set a task to remove the boots of speed after time out amount
		set_task(fTimeout, "taskBootsOfSpeedRemove", TASK_BOOTSOFSPEED + id, "", 0, "a", 1);
		
		//set the players maxspeed to 400 so they run faster!
		set_user_maxspeed(id, gfBootsMaxSpeed);
		
		//play boots of speed sound
		emit_sound(id, CHAN_STATIC, gszBootsOfSpeedSound, 1.0, ATTN_NORM, 0, PITCH_NORM);
		
		gfBootsOfSpeedTimeOut[id] = fTime + fTimeout;
		gfBootsOfSpeedNextUse[id] = fTime + fTimeout + get_cvar_float("bm_bootsofspeedcooldown");
	}
	else
	{
		set_hudmessage(gHudRed, gHudGreen, gHudBlue, gfTextX, gfTextY, gHudEffects, gfHudFxTime, gfHudHoldTime, gfHudFadeInTime, gfHudFadeOutTime, gHudChannel);
		show_hudmessage(id, "Boots of speed next use: %.1f", gfBootsOfSpeedNextUse[id] - fTime);
	}
}

actionTeleport(id, ent)
{
	//get end entity id
	new tele = entity_get_int(ent, EV_INT_iuser1);
	
	//if teleport end id is valid
	if (tele)
	{
		//get end entity origin
		new Float:vTele[3];
		entity_get_vector(tele, EV_VEC_origin, vTele);
		
		//if id of entity being teleported is a player and telefrags CVAR is set then kill any nearby players
		if ((id > 0 && id <= 32) && get_cvar_num("bm_telefrags") > 0)
		{
			new player = -1;
			
			do
			{
				player = find_ent_in_sphere(player, vTele, 16.0);
				
				//if entity found is a player
				if (player > 0 && player <= 32)
				{
					//if player is alive, and is not the player that went through the teleport
					if (is_user_alive(player) && player != id)
					{
						//kill the player
						user_kill(player, 1);
					}
				}
			}while(player);
		}
		
		//get origin of the start of the teleport
		new Float:vOrigin[3];
		new origin[3];
		entity_get_vector(ent, EV_VEC_origin, vOrigin);
		FVecIVec(vOrigin, origin);
		
		//show some teleporting effects
		message_begin(MSG_PVS, SVC_TEMPENTITY, origin);
		write_byte(TE_IMPLOSION);
		write_coord(origin[0]);
		write_coord(origin[1]);
		write_coord(origin[2]);
		write_byte(64);		// radius
		write_byte(100);	// count
		write_byte(6);		// life
		message_end();
		
		//teleport player
		entity_set_vector(id, EV_VEC_origin, vTele);
		
		//reverse players Z velocity
		new Float:vVelocity[3];
		entity_get_vector(id, EV_VEC_velocity, vVelocity);
		vVelocity[2] = floatabs(vVelocity[2]);
		entity_set_vector(id, EV_VEC_velocity, vVelocity);
		
		//play teleport sound
		emit_sound(id, CHAN_STATIC, gszTeleportSound, 1.0, ATTN_NORM, 0, PITCH_NORM);
	}
}

/***** TASKS *****/
public taskSolidNot(ent)
{
	ent -= TASK_BHOPSOLIDNOT;
	
	//make sure entity is valid
	if (is_valid_ent(ent))
	{
		//if block isn't being grabbed
		if (entity_get_int(ent, EV_INT_iuser2) == 0)
		{
			entity_set_int(ent, EV_INT_solid, SOLID_NOT);
			set_rendering(ent, kRenderFxNone, 255, 255, 255, kRenderTransAdd, 25);
			set_task(1.0, "taskSolid", TASK_BHOPSOLID + ent);
		}
	}
}

public taskSolid(ent)
{
	ent -= TASK_BHOPSOLID;
	
	//make sure entity is valid
	if (is_valid_ent(ent))
	{
		entity_set_int(ent, EV_INT_solid, SOLID_BBOX);
		
		//get the player ID of who has the block in a group (if anyone)
		new player = entity_get_int(ent, EV_INT_iuser1);
		
		//if the block is in a group
		if (player > 0)
		{
			//set the block so it is now 'being grouped' (for setting the rendering)
			groupBlock(0, ent);
		}
		else
		{
			set_rendering(ent, kRenderFxNone, 0, 0, 0, kRenderNormal, 255);
		}
	}
}

public taskInvincibleRemove(id)
{
	id -= TASK_INVINCIBLE;
	
	//make sure player is alive
	if (is_user_alive(id))
	{
		set_user_godmode(id, 0);
		
		//only set players rendering back to normal if player is not stealth
		if (halflife_time() >= gfStealthTimeOut[id])
		{
			set_user_rendering(id, kRenderFxGlowShell, 0, 0, 0, kRenderNormal, 16);
		}
	}
}

public taskStealthRemove(id)
{
	id -= TASK_STEALTH;
	
	//make sure player is connected
	if (is_user_connected(id))
	{
		//only set players rendering back to normal if player is not invincible
		if (halflife_time() >= gfInvincibleTimeOut[id])
		{	
			set_user_rendering(id, kRenderFxGlowShell, 0, 0, 0, kRenderNormal, 255);
		}
		else	//if player is invincible then set player to glow white
		{
			set_user_rendering(id, kRenderFxGlowShell, 255, 255, 255, kRenderTransColor, 16);
		}
	}
}

public taskNotOnIce(id)
{
	id -= TASK_ICE;
	
	//make player run normally
	entity_set_float(id, EV_FL_friction, 1.0);
	
	if (gfOldMaxSpeed[id] > 100.0)
	{
		set_user_maxspeed(id, gfOldMaxSpeed[id]);
	}
	else
	{
		set_user_maxspeed(id, 250.0);
	}
	
	//no longer 'on ice'
	gbOnIce[id] = false;
	gfOldMaxSpeed[id] = 0.0;
}

public taskCamouflageRemove(id)
{
	id -= TASK_CAMOUFLAGE;
	
	//if player is still connected
	if (is_user_connected(id))
	{
		//change back to players old model
		cs_set_user_model(id, gszCamouflageOldModel[id]);
	}
}

public taskNotInHoney(id)
{
	id -= TASK_HONEY;
	
	//if player is alive
	if (is_user_alive(id))
	{
		//make player move normally
		set_user_maxspeed(id, 250.0);
		
		//this will set the players maxspeed faster if they have the boots of speed!
		eventCurWeapon(id);
	}
}

public taskBootsOfSpeedRemove(id)
{
	id -= TASK_BOOTSOFSPEED;
	
	//set players speed back to normal
	set_user_maxspeed(id, 250.0);
}

public taskSpriteNextFrame(params[])
{
	new ent = params[0];
	
	//make sure entity is valid
	if (is_valid_ent(ent))
	{
		new frames = params[1];
		new Float:current_frame = entity_get_float(ent, EV_FL_frame);
		
		if (current_frame < 0.0 || current_frame >= frames)
		{
			entity_set_float(ent, EV_FL_frame, 1.0);
		}
		else
		{
			entity_set_float(ent, EV_FL_frame, current_frame + 1.0);
		}
	}
	else
	{
		remove_task(TASK_SPRITE + ent);
	}
}

/***** COMMANDS *****/
public cmdJump(id)
{
	//if the object the player is grabbing isn't too close
	if (gfGrablength[id] > 72.0)
	{
		//move the object closer
		gfGrablength[id] -= 16.0;
	}
}

public cmdDuck(id)
{
	//move the object further away
	gfGrablength[id] += 16.0;
}

public cmdAttack(id)
{
	//if entity being grabbed is a block
	if (isBlock(gGrabbed[id]))
	{
		//if block the player is grabbing is in their group and group count is larger than 1
		if (isBlockInGroup(id, gGrabbed[id]) && gGroupCount[id] > 1)
		{
			new block;
			
			//move the rest of the blocks in the players group using vector for grabbed block
			for (new i = 0; i <= gGroupCount[id]; ++i)
			{
				block = gGroupedBlocks[id][i];
				
				//if this block is in this players group
				if (isBlockInGroup(id, block))
				{
					//only copy the block if it is not stuck
					if (!isBlockStuck(block))
					{
						//copy the block
						copyBlock(block);
					}
				}
			}
		}
		else
		{
			//only copy the block the player has grabbed if it is not stuck
			if (!isBlockStuck(gGrabbed[id]))
			{
				//copy the block
				new newBlock = copyBlock(gGrabbed[id]);
				
				//if new block was created successfully
				if (newBlock)
				{
					//set currently grabbed block to 'not being grabbed'
					entity_set_int(gGrabbed[id], EV_INT_iuser2, 0);
					
					//set new block to 'being grabbed'
					entity_set_int(newBlock, EV_INT_iuser2, id);
					
					//set player to grabbing new block
					gGrabbed[id] = newBlock;
				}
			}
			else
			{
				//tell the player they can't copy a block when it is in a stuck position
				client_print(id, print_chat, "%sYou cannot copy a block that is in a stuck position!", gszPrefix);
			}
		}
	}
}

public cmdAttack2(id)
{
	//if player is grabbing a block
	if (isBlock(gGrabbed[id]))
	{
		//if block the player is grabbing is in their group and group count is larger than 1
		if (isBlockInGroup(id, gGrabbed[id]) && gGroupCount[id] > 1)
		{
			new block;
			
			//iterate through all blocks in the players group
			for (new i = 0; i <= gGroupCount[id]; ++i)
			{
				block = gGroupedBlocks[id][i];
				
				//if block is still valid
				if (is_valid_ent(block))
				{
					//if block is still in this players group
					if (isBlockInGroup(id, block))
					{
						//delete the block
						gbJustDeleted[id] = deleteBlock(block);
					}
				}
			}
		}
		else
		{
			//delete the block
			gbJustDeleted[id] = deleteBlock(gGrabbed[id]);
		}
	}
	//if player is grabbing a teleport
	else if (isTeleport(gGrabbed[id]))
	{
		//delete the teleport
		gbJustDeleted[id] = deleteTeleport(id, gGrabbed[id]);
	}
}

public cmdGrab(id)
{
	//make sure player has access to use this command
	if (get_user_flags(id) & BM_ADMIN_LEVEL)
	{
		//get the entity the player is aiming at and the length
		new ent, body;
		gfGrablength[id] = get_user_aiming(id, ent, body);
		
		//get if the entity is a block or a teleport
		new bool:bIsBlock = isBlock(ent);
		new bool:bIsTeleport = isTeleport(ent);
		
		//if the entity is a block or teleport
		if (bIsBlock || bIsTeleport)
		{
			//get who is currently grabbing the entity (if anyone)
			new grabber = entity_get_int(ent, EV_INT_iuser2);
			
			//if entity is not being grabbed by someone else
			if (grabber == 0 || grabber == id)
			{
				//if entity is a block
				if (bIsBlock)
				{
					//get the player ID of who has the block in a group (if anyone)
					new player = entity_get_int(ent, EV_INT_iuser1);
					
					//if the block is not in a group or is in this players group
					if (player == 0 || player == id)
					{
						//set the block to 'being grabbed'
						setGrabbed(id, ent);
						
						//if this block is in this players group and group count is greater than 1
						if (player == id && gGroupCount[id] > 1)
						{
							new Float:vGrabbedOrigin[3];
							new Float:vOrigin[3];
							new Float:vOffset[3];
							new block;
							
							//get origin of the block
							entity_get_vector(ent, EV_VEC_origin, vGrabbedOrigin);
							
							//iterate through all blocks in players group
							for (new i = 0; i <= gGroupCount[id]; ++i)
							{
								block = gGroupedBlocks[id][i];
								
								//if block is still valid
								if (is_valid_ent(block))
								{
									player = entity_get_int(ent, EV_INT_iuser1);
									
									//if block is still in this players group
									if (player == id)
									{
										//get origin of block in players group
										entity_get_vector(block, EV_VEC_origin, vOrigin);
										
										//calculate offset from grabbed block
										vOffset[0] = vGrabbedOrigin[0] - vOrigin[0];
										vOffset[1] = vGrabbedOrigin[1] - vOrigin[1];
										vOffset[2] = vGrabbedOrigin[2] - vOrigin[2];
										
										//save offset value in grouped block
										entity_set_vector(block, EV_VEC_vuser1, vOffset);
										
										//indicate that entity is being grabbed
										entity_set_int(block, EV_INT_iuser2, id);
									}
								}
							}
						}
					}
				}
				//if entity is a teleporter
				else if (bIsTeleport)
				{
					//set the teleport to 'being grabbed'
					setGrabbed(id, ent);
				}
			}
		}
	}
	
	return PLUGIN_HANDLED;
}

setGrabbed(id, ent)
{
	new Float:fpOrigin[3];
	new Float:fbOrigin[3];
	new Float:fAiming[3];
	new iAiming[3];
	new bOrigin[3];
	
	//get players current view model then clear it
	entity_get_string(id, EV_SZ_viewmodel, gszViewModel[id], 32);
	entity_set_string(id, EV_SZ_viewmodel, "");
	
	get_user_origin(id, bOrigin, 1);			//position from eyes (weapon aiming)
	get_user_origin(id, iAiming, 3);			//end position from eyes (hit point for weapon)
	entity_get_vector(id, EV_VEC_origin, fpOrigin);		//get player position
	entity_get_vector(ent, EV_VEC_origin, fbOrigin);	//get block position
	IVecFVec(iAiming, fAiming);
	FVecIVec(fbOrigin, bOrigin);
	
	//set gGrabbed
	gGrabbed[id] = ent;
	gfGrabOffset[id][0] = fbOrigin[0] - iAiming[0];
	gfGrabOffset[id][1] = fbOrigin[1] - iAiming[1];
	gfGrabOffset[id][2] = fbOrigin[2] - iAiming[2];
	
	//indicate that entity is being grabbed
	entity_set_int(ent, EV_INT_iuser2, id);
}

public cmdRelease(id)
{
	//make sure player has access to use this command
	if (get_user_flags(id) & BM_ADMIN_LEVEL)
	{
		//if player is grabbing an entity
		if (gGrabbed[id])
		{
			//if entity player is grabbing is a block
			if (isBlock(gGrabbed[id]))
			{
				//if block the player is grabbing is in their group and group count is > 1
				if (isBlockInGroup(id, gGrabbed[id]) && gGroupCount[id] > 1)
				{
					new block;
					new bool:bGroupIsStuck = true;
					
					//iterate through all blocks in the players group
					for (new i = 0; i <= gGroupCount[id]; ++i)
					{
						block = gGroupedBlocks[id][i];
						
						//if this block is in this players group
						if (isBlockInGroup(id, block))
						{
							//indicate that entity is no longer being grabbed
							entity_set_int(block, EV_INT_iuser2, 0);
							
							//start off thinking all blocks in group are stuck
							if (bGroupIsStuck)
							{
								//if block is not stuck
								if (!isBlockStuck(block))
								{
									//at least one of the blocks in the group are not stuck
									bGroupIsStuck = false;
								}
							}
						}
					}
					
					//if all the blocks in the group are stuck
					if (bGroupIsStuck)
					{
						//iterate through all blocks in the players group
						for (new i = 0; i <= gGroupCount[id]; ++i)
						{
							block = gGroupedBlocks[id][i];
							
							//if this block is in this players group
							if (isBlockInGroup(id, block))
							{
								//delete the block
								deleteBlock(block);
							}
						}
						
						//tell the player all the blocks were deleted because they were stuck
						client_print(id, print_chat, "%sGroup deleted because all the blocks were stuck!", gszPrefix);
					}
				}
				else
				{
					//if block player has grabbed is valid
					if (is_valid_ent(gGrabbed[id]))
					{
						//if the block is stuck
						if (isBlockStuck(gGrabbed[id]))
						{
							//delete the block
							new bool:bDeleted = deleteBlock(gGrabbed[id]);
							
							//if the block was deleted successfully
							if (bDeleted)
							{
								//tell the player the block was deleted and why
								client_print(id, print_chat, "%sBlock deleted because it was stuck!", gszPrefix);
							}
						}
						else
						{
							//indicate that the block is no longer being grabbed
							entity_set_int(gGrabbed[id], EV_INT_iuser2, 0);
						}
					}
				}
			}
			else if (isTeleport(gGrabbed[id]))
			{
				//indicate that the teleport is no longer being grabbed
				entity_set_int(gGrabbed[id], EV_INT_iuser2, 0);
			}
			
			//set the players view model back to what it was
			entity_set_string(id, EV_SZ_viewmodel, gszViewModel[id]);
			
			//indicate that player is not grabbing an object
			gGrabbed[id] = 0;
		}
	}
	
	return PLUGIN_HANDLED;
}

/* MENUS */
public cmdShowMainMenu(id)
{
	new col[3];
	new szMenu[256];
	new szGodmode[6];
	new szNoclip[6];
	col = (get_user_flags(id) & BM_ADMIN_LEVEL ? "\w" : "\d");
	szGodmode = (get_user_godmode(id) ? "\yOn" : "\rOff");
	szNoclip = (get_user_noclip(id) ? "\yOn" : "\rOff");
	
	//format the main menu
	format(szMenu, 256, gszMenu, gszBlockNames[gSelectedBlockType[id]], col, col, col, col, col, szNoclip, col, szGodmode);
	
	//show the main menu to the player
	show_menu(id, MAIN_MENU_KEYS, szMenu, -1, "bmMainMenu");
	
	return PLUGIN_HANDLED;
}

showBlockSelectionMenu(id)
{
	//create block selection menu 1 (first 8 blocks)
	new szBlockMenu[256];
	new szTitle[32];
	new szEntry[32];
	new num;
	new startBlock;
	
	//format the page number into the menu title
	format(szTitle, sizeof(szTitle), "\yBlock Selection %d^n^n", gBlockMenuPage[id]);
	
	//add the title to the menu
	add(szBlockMenu, sizeof(szBlockMenu), szTitle);
	
	//calculate the block that the menu will start on
	startBlock = (gBlockMenuPage[id] - 1) * 8;
	
	//iterate through 8 blocks to add to the menu
	for (new i = startBlock; i < startBlock + 8; ++i)
	{
		//make sure the loop doesn't go above the maximum number of blocks
		if (i < gBlockMax)
		{
			//calculate the menu item number
			num = (i - startBlock) + 1;
			
			//format the block name into the menu entry
			format(szEntry, sizeof(szEntry), "\r%d. \w%s^n", num, gszBlockNames[i]);
		}
		else
		{
			//format a blank menu entry
			format(szEntry, sizeof(szEntry), "^n");
		}
		
		//add the entry to the menu
		add(szBlockMenu, sizeof(szBlockMenu), szEntry);
	}
	
	//if the block selection page the player is on is less than the maximum page
	if (gBlockMenuPage[id] < gBlockMenuPagesMax)
	{
		add(szBlockMenu, sizeof(szBlockMenu), "^n\r9. \wMore");
	}
	else
	{
		add(szBlockMenu, sizeof(szBlockMenu), "^n");
	}
	
	//add a back option to the menu
	add(szBlockMenu, sizeof(szBlockMenu), "^n\r0. \wBack");
	
	//display the block selection menu
	show_menu(id, BLOCKSELECTION_MENU_KEYS, szBlockMenu, -1, "bmBlockSelectionMenu");
}

showOptionsMenu(id, oldMenu)
{
	//set the oldmenu global variable so when the back key is pressed it goes back to the previous menu
	gMenuBeforeOptions[id] = oldMenu;
	
	new col[3];
	new szSnapping[6];
	new szMenu[256];
	col = (get_user_flags(id) & BM_ADMIN_LEVEL ? "\w" : "\d");
	szSnapping = (gbSnapping[id] ? "\yOn" : "\rOff");
	
	//format the options menu
	format(szMenu, sizeof(szMenu), gszOptionsMenu, col, szSnapping, col, gfSnappingGap[id], col, col, col, col, col, col);
	
	//show the options menu to player
	show_menu(id, OPTIONS_MENU_KEYS, szMenu, -1, "bmOptionsMenu");
}

showTeleportMenu(id)
{
	new col[3];
	new szMenu[256];
	new szGodmode[6];
	new szNoclip[6];
	col = (get_user_flags(id) & BM_ADMIN_LEVEL ? "\w" : "\d");
	szGodmode = (get_user_godmode(id) ? "\yOn" : "\rOff");
	szNoclip = (get_user_noclip(id) ? "\yOn" : "\rOff");
	
	//format teleport menu
	format(szMenu, sizeof(szMenu), gszTeleportMenu, col, (gTeleportStart[id] ? "\w" : "\d"), col, col, col, szNoclip, col, szGodmode);
	
	show_menu(id, TELEPORT_MENU_KEYS, szMenu, -1, "bmTeleportMenu");
}

showChoiceMenu(id, gChoice, const szTitle[96])
{
	gChoiceOption[id] = gChoice;
	
	//format choice menu using given title
	new szMenu[128];
	format(szMenu, sizeof(szMenu), gszChoiceMenu, szTitle);
	
	//show the choice menu to the player
	show_menu(id, TELEPORT_MENU_KEYS, szMenu, -1, "bmChoiceMenu");
}

public handleMainMenu(id, num)
{
	switch (num)
	{
		case N1: { showBlockSelectionMenu(id); }
		case N2: { createBlockAiming(id, gSelectedBlockType[id]); }
		case N3: { convertBlockAiming(id, gSelectedBlockType[id]); }
		case N4: { deleteBlockAiming(id); }
		case N5: { rotateBlockAiming(id); }
		case N6: { toggleNoclip(id); }
		case N7: { toggleGodmode(id); }
		case N8: { showOptionsMenu(id, 1); }
		case N9: { showTeleportMenu(id); }
		case N0: { return; }
	}
	
	//selections 1, 8 and 9 show different menus
	if (num != N1 && num != N8 && num != N9)
	{
		//display menu again
		cmdShowMainMenu(id);
	}
}

public handleBlockSelectionMenu(id, num)
{
	switch (num)
	{
		case N9:
		{
			//goto next block selection menu page
			++gBlockMenuPage[id];
			
			//make sure the player can't go above the maximum block selection page
			if (gBlockMenuPage[id] > gBlockMenuPagesMax)
			{
				gBlockMenuPage[id] = gBlockMenuPagesMax;
			}
			
			//show block selection menu again
			showBlockSelectionMenu(id);
		}
		
		case N0:
		{
			//goto previous block selection menu page
			--gBlockMenuPage[id];
			
			//show main menu if player goes back too far
			if (gBlockMenuPage[id] < 1)
			{
				cmdShowMainMenu(id);
				gBlockMenuPage[id] = 1;
			}
			else
			{
				//show block selection menu again
				showBlockSelectionMenu(id);
			}
		}
		
		default:
		{
			//offset the num value using the players block selection page number
			num += (gBlockMenuPage[id] - 1) * 8;
			
			//if block number is within range
			if (num < gBlockMax)
			{
				gSelectedBlockType[id] = num;
				cmdShowMainMenu(id);
			}
			else
			{
				showBlockSelectionMenu(id);
			}
		}
	}
}

public handleOptionsMenu(id, num)
{
	switch (num)
	{
		case N1: { toggleSnapping(id); }
		case N2: { toggleSnappingGap(id); }
		case N3: { groupBlockAiming(id); }
		case N4: { groupClear(id); }
		case N5: { showChoiceMenu(id, CHOICE_DEL_BLOCKS, "Are you sure you want to erase all blocks on the map?"); }
		case N6: { showChoiceMenu(id, CHOICE_DEL_TELEPORTS, "Are you sure you want to erase all teleports on the map?"); }
		case N7: { saveBlocks(id); }
		case N8: { showChoiceMenu(id, CHOICE_LOAD, "Loading will erase all blocks and teleports, do you want to continue?"); }
		case N9: { showHelp(id); }
		
		case N0:
		{
			if (gMenuBeforeOptions[id] == 1)
			{
				cmdShowMainMenu(id);
			}
			else if (gMenuBeforeOptions[id] == 2)
			{
				showTeleportMenu(id);
			}
			else
			{
				//for some reason the players 'gMenuBeforeOptions' number is invalid
				log_amx("%sPlayer ID: %d has an invalid gMenuBeforeOptions: %d", gszPrefix, id, gMenuBeforeOptions[id]);
			}
		}
	}
	
	//these selections show a different menu
	if (num != N5 && num != N6 && num != N8 && num != N0)
	{
		//display menu again
		showOptionsMenu(id, gMenuBeforeOptions[id]);
	}
}

public handleTeleportMenu(id, num)
{
	switch (num)
	{
		case N1: { createTeleportAiming(id, TELEPORT_START); }
		case N2: { createTeleportAiming(id, TELEPORT_END); }
		case N3: { showTeleportPath(id); }
		case N4: { deleteTeleportAiming(id); }
		case N6: { toggleNoclip(id); }
		case N7: { toggleGodmode(id); }
		case N8: { showOptionsMenu(id, 2); }
		case N0: { cmdShowMainMenu(id); }
	}
	
	//selections 6, 8 and 9 show different menus
	if (num != N6 && num != N8 && num != N0)
	{
		showTeleportMenu(id);
	}
}

public handleChoiceMenu(id, num)
{
	switch (num)
	{
		case N1:	//YES
		{
			switch (gChoiceOption[id])
			{
				case CHOICE_LOAD: loadBlocks(id);
				case CHOICE_DEL_BLOCKS: deleteAllBlocks(id, true);
				case CHOICE_DEL_TELEPORTS: deleteAllTeleports(id, true);
			
				default:
				{
					log_amx("%sInvalid choice in handleChoiceMenu()", gszPrefix);
				}
			}
		}
	}
	
	//show options menu again
	showOptionsMenu(id, gMenuBeforeOptions[id]);
}

toggleGodmode(id)
{
	//make sure player has access to this command
	if (get_user_flags(id) & BM_ADMIN_LEVEL)
	{
		//if player has godmode
		if (get_user_godmode(id))
		{
			//turn off godmode for player
			set_user_godmode(id, 0);
			gbAdminGodmode[id] = false;
		}
		else
		{
			//turn on godmode for player
			set_user_godmode(id, 1);
			gbAdminGodmode[id] = true;
		}
	}
}

toggleNoclip(id)
{
	//make sure player has access to this command
	if (get_user_flags(id) & BM_ADMIN_LEVEL)
	{
		//if player has noclip
		if (get_user_noclip(id))
		{
			//turn off noclip for player
			set_user_noclip(id, 0);
			gbAdminNoclip[id] = false;
		}
		else
		{
			//turn on noclip for player
			set_user_noclip(id, 1);
			gbAdminNoclip[id] = true;
		}
	}
}

toggleSnapping(id)
{
	//make sure player has access to this command
	if (get_user_flags(id) & BM_ADMIN_LEVEL)
	{
		gbSnapping[id] = !gbSnapping[id];
	}
}

toggleSnappingGap(id)
{
	//make sure player has access to this command
	if (get_user_flags(id) & BM_ADMIN_LEVEL)
	{
		//increment this players snapping gap by 5
		gfSnappingGap[id] += 4.0;
		
		//if this players snapping gap gets too big then loop it back to 0
		if (gfSnappingGap[id] > 40.0)
		{
			gfSnappingGap[id] = 0.0;
		}
	}
}

showHelp(id)
{
	//get cvar values
	new szHelpText[1600];
	
	new Telefrags = get_cvar_num("bm_telefrags");
	new Float:fFireDamageAmount = get_cvar_float("bm_firedamageamount");
	new Float:fDamageAmount = get_cvar_float("bm_damageamount");
	new Float:fHealAmount = get_cvar_float("bm_healamount");
	new Float:fInvincibleTime = get_cvar_float("bm_invincibletime");
	new Float:fInvincibleCooldown = get_cvar_float("bm_invinciblecooldown");
	new Float:fStealthTime = get_cvar_float("bm_stealthtime");
	new Float:fStealthCooldown = get_cvar_float("bm_stealthcooldown");
	new Float:fCamouflageTime = get_cvar_float("bm_camouflagetime");
	new Float:fCamouflageCooldown = get_cvar_float("bm_camouflagecooldown");
	new Float:fNukeCooldown = get_cvar_float("bm_nukecooldown");
	new Float:fRandomCooldown = get_cvar_float("bm_randomcooldown");
	new Float:fBootsOfSpeedTime = get_cvar_float("bm_bootsofspeedtime");
	new Float:fBootsOfSpeedCooldown = get_cvar_float("bm_bootsofspeedcooldown");
	
	//format the help text
	format(szHelpText, sizeof(szHelpText), gszHelpText, Telefrags, fFireDamageAmount, fDamageAmount, fHealAmount, fInvincibleTime, fInvincibleCooldown, fStealthTime, fStealthCooldown, fCamouflageTime, fCamouflageCooldown, fNukeCooldown, fRandomCooldown, fBootsOfSpeedTime, fBootsOfSpeedCooldown);
	
	//show the help
	show_motd(id, szHelpText, gszHelpTitle);
}

showTeleportPath(id)
{
	//get the entity the player is aiming at
	new ent, body;
	get_user_aiming(id, ent, body);
	
	//if entity found is a teleport
	if (isTeleport(ent))
	{
		//get other side of teleport
		new tele = entity_get_int(ent, EV_INT_iuser1);
		
		//if there is another end to the teleport
		if (tele)
		{
			//get origins of the start and end teleport entities
			new life = 50;
			new Float:vOrigin1[3];
			new Float:vOrigin2[3];
			entity_get_vector(ent, EV_VEC_origin, vOrigin1);
			entity_get_vector(tele, EV_VEC_origin, vOrigin2);
			
			//draw a line in between the 2 origins
			drawLine(vOrigin1, vOrigin2, life);
			
			//get the distance between the points
			new Float:fDist = get_distance_f(vOrigin1, vOrigin2);
			
			//notify that a line has been drawn between the start and end of the teleport
			client_print(id, print_chat, "%sA line has been drawn to show the teleport path. Distance: %f units.", gszPrefix, fDist);
		}
	}
}

/* GROUPING BLOCKS */
groupBlockAiming(id)
{
	//get the entity the player is aiming at
	new ent, body;
	get_user_aiming(id, ent, body);
	
	//is entity is a block
	if (isBlock(ent))
	{
		//get whether or not block is already being grouped
		new player = entity_get_int(ent, EV_INT_iuser1);
		
		//if block is not in a group
		if (player == 0)
		{
			//increment group value
			++gGroupCount[id];
			
			//add this entity to the players group
			gGroupedBlocks[id][gGroupCount[id]] = ent;
			
			//set the block so it is now 'being grouped'
			groupBlock(id, ent);
			
		}
		//if block is in this players group
		else if (player == id)
		{
			//remove block from being grouped
			groupRemoveBlock(ent);
		}
		//if another player has the block grouped
		else
		{
			//get id and name of who has the block grouped
			new szName[32];
			new player = entity_get_int(ent, EV_INT_iuser1);
			get_user_name(player, szName, 32);
			
			//notify player who the block is being grouped by
			client_print(id, print_chat, "%sBlock is already in a group by: %s", gszPrefix, szName);
		}
	}
}

groupBlock(id, ent)
{
	//if entity is valid
	if (is_valid_ent(ent))
	{
		//if id passed in is a player
		if (id > 0 && id <= 32)
		{
			//set block so it is now being grouped
			entity_set_int(ent, EV_INT_iuser1, id);
		}
		
		//get block type
		new blockType = entity_get_int(ent, EV_INT_body);
		
		//if block is a stealth block
		if (blockType == BM_STEALTH)
		{
			//make block transparent and red
			set_rendering(ent, kRenderFxGlowShell, 255, 0, 0, kRenderTransAdd, 16);
		}
		else
		{
			//make block glow red to show it is grouped
			set_rendering(ent, kRenderFxGlowShell, 255, 0, 0, kRenderNormal, 16);
		}
	}
}

groupRemoveBlock(ent)
{
	//make sure entity is a block
	if (isBlock(ent))
	{
		//remove block from being grouped (stays in players gGroupedBlocks[id][] array
		entity_set_int(ent, EV_INT_iuser1, 0);
		
		//get block type
		new blockType = entity_get_int(ent, EV_INT_body);
		
		//if block is an invincibility block
		if (blockType == BM_INVINCIBILITY)
		{
			//set block to glow white
			set_rendering(ent, kRenderFxGlowShell, 255, 255, 255, kRenderNormal, 16);
		}
		else if (blockType == BM_STEALTH)
		{
			//set block to be transparent
			set_rendering(ent, kRenderFxNone, 255, 255, 255, kRenderTransAdd, 180);
		}
		else
		{
			//remove glow on block
			set_rendering(ent, kRenderFxNone, 0, 0, 0, kRenderNormal, 255);
		}
	}
}

groupClear(id)
{
	new blockCount = 0;
	new blocksDeleted = 0;
	new block;
	
	//remove all players blocks from being grouped
	for (new i = 0; i <= gGroupCount[id]; ++i)
	{
		block = gGroupedBlocks[id][i];
		
		//if block is in this players group
		if (isBlockInGroup(id, block))
		{
			//if block is stuck
			if (isBlockStuck(block))
			{
				//delete the stuck block
				deleteBlock(block);
				
				//count how many blocks have been deleted
				++blocksDeleted;
			}
			else
			{
				//remove block from being grouped
				groupRemoveBlock(block);
				
				//count how many blocks have been removed from group
				++blockCount;
			}
		}
	}
	
	//set players group count to 0
	gGroupCount[id] = 0;
	
	//if player is connected
	if (is_user_connected(id))
	{
		//if some blocks were deleted
		if (blocksDeleted > 0)
		{
			//notify player how many blocks were cleared from group and deleted
			client_print(id, print_chat, "%sRemoved %d blocks from group, deleted %d stuck blocks", gszPrefix, blockCount, blocksDeleted);
		}
		else
		{
			//notify player how many blocks were cleared from group
			client_print(id, print_chat, "%sRemoved %d blocks from group", gszPrefix, blockCount);
		}
	}
}

/* BLOCK & TELEPORT OPERATIONS */
moveGrabbedEntity(id, Float:vMoveTo[3] = {0.0, 0.0, 0.0})
{
	new iOrigin[3], iLook[3];
	new Float:fOrigin[3], Float:fLook[3], Float:fDirection[3], Float:fLength;
	
	get_user_origin(id, iOrigin, 1);		//Position from eyes (weapon aiming)
	get_user_origin(id, iLook, 3);			//End position from eyes (hit point for weapon)
	IVecFVec(iOrigin, fOrigin);
	IVecFVec(iLook, fLook);
	
	fDirection[0] = fLook[0] - fOrigin[0];
	fDirection[1] = fLook[1] - fOrigin[1];
	fDirection[2] = fLook[2] - fOrigin[2];
	fLength = get_distance_f(fLook, fOrigin);
	
	if (fLength == 0.0) fLength = 1.0;		//avoid division by 0
	
	//calculate the position to move the block
	vMoveTo[0] = (fOrigin[0] + fDirection[0] * gfGrablength[id] / fLength) + gfGrabOffset[id][0];
	vMoveTo[1] = (fOrigin[1] + fDirection[1] * gfGrablength[id] / fLength) + gfGrabOffset[id][1];
	vMoveTo[2] = (fOrigin[2] + fDirection[2] * gfGrablength[id] / fLength) + gfGrabOffset[id][2];
	vMoveTo[2] = float(floatround(vMoveTo[2], floatround_floor));
	
	//move the block and its sprite (if it has one)
	moveEntity(id, gGrabbed[id], vMoveTo, true);
}

moveEntity(id, ent, Float:vMoveTo[3], bool:bDoSnapping)
{
	//do snapping for entity if snapping boolean passed in is true
	if (bDoSnapping)
	{
		doSnapping(id, ent, vMoveTo);
	}
	
	//set the position of the block
	entity_set_origin(ent, vMoveTo);
	
	//get the sprite that sits above the block (if any)
	new sprite = entity_get_int(ent, EV_INT_iuser3);
	
	//if sprite entity is valid
	if (sprite)
	{
		//get size of block
		new Float:vSizeMax[3];
		entity_get_vector(ent, EV_VEC_maxs, vSizeMax);
		
		//move the sprite onto the top of the block
		vMoveTo[2] += vSizeMax[2] + 0.15;
		entity_set_origin(sprite, vMoveTo);
	}
}

/* TELEPORTS */
createTeleportAiming(id, teleportType)
{
	//make sure player has access to this command
	if (get_user_flags(id) & BM_ADMIN_LEVEL)
	{
		//get where player is aiming for origin of teleport entity
		new pOrigin[3], Float:vOrigin[3];
		get_user_origin(id, pOrigin, 3);
		IVecFVec(pOrigin, vOrigin);
		vOrigin[2] += gfTeleportZOffset;
		
		//create the teleport of the given type
		createTeleport(id, teleportType, vOrigin);
	}
}

createTeleport(id, teleportType, Float:vOrigin[3])
{
	new ent = create_entity(gszInfoTarget);
	
	if (is_valid_ent(ent))
	{
		switch (teleportType)
		{
			case TELEPORT_START:
			{
				//if player has already created a teleport start entity then delete it
				if (gTeleportStart[id])
				{
					remove_entity(gTeleportStart[id]);
				}
				
				//set teleport properties
				entity_set_string(ent, EV_SZ_classname, gszTeleportStartClassname);
				entity_set_int(ent, EV_INT_solid, SOLID_BBOX);
				entity_set_int(ent, EV_INT_movetype, MOVETYPE_NONE);
				entity_set_model(ent, gszTeleportSpriteStart);
				entity_set_size(ent, gfTeleportSizeMin, gfTeleportSizeMax);
				entity_set_origin(ent, vOrigin);
				
				//set the rendermode and transparency
				entity_set_int(ent, EV_INT_rendermode, 5);	//rendermode
				entity_set_float(ent, EV_FL_renderamt, 255.0);	//visable 
				
				//set task for animating sprite
				new params[2];
				params[0] = ent;
				params[1] = gTeleportStartFrames;
				set_task(0.1, "taskSpriteNextFrame", TASK_SPRITE + ent, params, 2, "b");
				
				//store teleport start entity to a global variable so it can be linked to the end entity 
				gTeleportStart[id] = ent;
			}
			
			case TELEPORT_END:
			{
				//make sure there is a teleport start entity
				if (gTeleportStart[id])
				{
					//set teleport properties
					entity_set_string(ent, EV_SZ_classname, gszTeleportEndClassname);
					entity_set_int(ent, EV_INT_solid, SOLID_BBOX);
					entity_set_int(ent, EV_INT_movetype, MOVETYPE_NONE);
					entity_set_model(ent, gszTeleportSpriteEnd);
					entity_set_size(ent, gfTeleportSizeMin, gfTeleportSizeMax);
					entity_set_origin(ent, vOrigin);
					
					//set the rendermode and transparency
					entity_set_int(ent, EV_INT_rendermode, 5);	//rendermode
					entity_set_float(ent, EV_FL_renderamt, 255.0);	//visable 
					
					//link up teleport start and end entities
					entity_set_int(ent, EV_INT_iuser1, gTeleportStart[id]);
					entity_set_int(gTeleportStart[id], EV_INT_iuser1, ent);
					
					//set task for animating sprite
					new params[2];
					params[0] = ent;
					params[1] = gTeleportEndFrames;
					set_task(0.1, "taskSpriteNextFrame", TASK_SPRITE + ent, params, 2, "b");
					
					//indicate that this player has no teleport start entity waiting for an end
					gTeleportStart[id] = 0;
				}
				else
				{
					//delete entity that was created because there is no start entity
					remove_entity(ent);
				}
			}
		}
	}
	else
	{
		log_amx("%sCouldn't create 'env_sprite' entity", gszPrefix);
	}
}

deleteTeleportAiming(id)
{
	//make sure player has access to this command
	if (get_user_flags(id) & BM_ADMIN_LEVEL)
	{
		//get entity that player is aiming at
		new ent, body;
		get_user_aiming(id, ent, body, 9999);
		
		//delete block that player is aiming at
		new bool:deleted = deleteTeleport(id, ent);
		
		if (deleted)
		{
			client_print(id, print_chat, "%sTeleport deleted!", gszPrefix);
		}
	}
}

bool:deleteTeleport(id, ent)
{
	//iterate through the different types of teleport (start and end)
	for (new i = 0; i < 2; ++i)
	{
		//if entity is a teleport then delete both the start and the end of the teleport
		if (isTeleport(ent))
		{
			//get entity id of the other side of the teleport
			new tele = entity_get_int(ent, EV_INT_iuser1);
			
			//clear teleport start entity if it was just deleted
			if (gTeleportStart[id] == ent || gTeleportStart[id] == tele)
			{
				gTeleportStart[id] = 0;
			}
			
			//remove tasks that exist to animate the teleport sprites
			if (task_exists(TASK_SPRITE + ent))
			{
				remove_task(TASK_SPRITE + ent);
			}
			
			if (task_exists(TASK_SPRITE + tele))
			{
				remove_task(TASK_SPRITE + tele);
			}
			
			//delete both the start and end positions of the teleporter
			if (tele)
			{
				remove_entity(tele);
			}
			
			remove_entity(ent);
			
			//delete was deleted
			return true;
		}
	}
	
	//teleport was not deleted
	return false;
}

deleteAllBlocks(id, bool:bNotify)
{
	//make sure player has access to this command
	if (get_user_flags(id) & BM_ADMIN_LEVEL)
	{
		new bool:bDeleted;
		new blockCount = 0;
		new ent = -1;
		
		//find all blocks in the map
		while ((ent = find_ent_by_class(ent, gszBlockClassname)))
		{
			//delete the block
			bDeleted = deleteBlock(ent);
			
			//if block was successfully deleted
			if (bDeleted)
			{
				//increment counter for how many blocks have been deleted
				++blockCount;
			}
		}
		
		//get players name
		new szName[32];
		get_user_name(id, szName, 32);
		
		//iterate through all players
		for (new i = 1; i <= 32; ++i)
		{
			//make sure nobody is grabbing a block because they've all been deleted!
			gGrabbed[id] = 0;
			
			//make sure player is connected
			if (is_user_connected(i))
			{
				//notify all admins that the player deleted all the blocks
				if (bNotify && get_user_flags(i) & BM_ADMIN_LEVEL)
				{
					client_print(i, print_chat, "%s'%s' deleted all the blocks from the map. Total blocks: %d", gszPrefix, szName, blockCount);
				}
			}
		}
	}
}

deleteAllTeleports(id, bool:bNotify)
{
	//make sure player has access to this command
	if (get_user_flags(id) & BM_ADMIN_LEVEL)
	{
		new bool:bDeleted;
		new teleCount = 0;
		new ent = -1;
		
		//find all teleport start entities in the map
		while ((ent = find_ent_by_class(ent, gszTeleportStartClassname)))
		{
			//delete the block
			bDeleted = deleteTeleport(id, ent);
			
			//if block was successfully deleted
			if (bDeleted)
			{
				//increment counter for how many blocks have been deleted
				++teleCount;
			}
		}
		
		//get players name
		new szName[32];
		get_user_name(id, szName, 32);
		
		//iterate through all players
		for (new i = 1; i <= 32; ++i)
		{
			//make sure nobody has a teleport start set
			gTeleportStart[id] = 0;
			
			//make sure player is connected
			if (is_user_connected(i))
			{
				//notify all admins that the player deleted all the teleports
				if (bNotify && get_user_flags(i) & BM_ADMIN_LEVEL)
				{
					client_print(i, print_chat, "%s'%s' deleted all the teleports from the map. Total teleports: %d", gszPrefix, szName, teleCount);
				}
			}
		}
	}
}

/***** BLOCKS *****/
createBlockAiming(const id, const blockType)
{
	//make sure player has access to this command
	if (get_user_flags(id) & BM_ADMIN_LEVEL)
	{
		new origin[3];
		new Float:vOrigin[3];
		
		//get the origin of the player and add Z offset
		get_user_origin(id, origin, 3);
		IVecFVec(origin, vOrigin);
		vOrigin[2] += gfBlockSizeMaxForZ[2];
		
		//create the block
		createBlock(id, blockType, vOrigin, gfDefaultBlockAngles, gfBlockSizeMinForZ, gfBlockSizeMaxForZ);
	}
}

createBlock(const id, const blockType, Float:vOrigin[3], Float:vAngles[3], Float:vSizeMin[3], Float:vSizeMax[3])
{
	new ent = create_entity(gszInfoTarget);
	
	//make sure entity was created successfully
	if (is_valid_ent(ent))
	{
		//set block properties
		entity_set_string(ent, EV_SZ_classname, gszBlockClassname);
		entity_set_int(ent, EV_INT_solid, SOLID_BBOX);
		entity_set_int(ent, EV_INT_movetype, MOVETYPE_NONE);
		
		switch (blockType)
		{
			case BM_PLATFORM: entity_set_model(ent, gszBlockModelPlatform);
			case BM_BHOP: entity_set_model(ent, gszBlockModelBhop);
			case BM_DAMAGE: entity_set_model(ent, gszBlockModelDamage);
			case BM_HEALER: entity_set_model(ent, gszBlockModelHealer);
			case BM_INVINCIBILITY: entity_set_model(ent, gszBlockModelInvincibility);
			case BM_STEALTH: entity_set_model(ent, gszBlockModelDefault);
			case BM_TRAMPOLINE: entity_set_model(ent, gszBlockModelDefault);
			case BM_SPEEDBOOST: entity_set_model(ent, gszBlockModelSpeedBoost);
			case BM_NOFALLDAMAGE: entity_set_model(ent, gszBlockModelNoFallDamage);
			case BM_ICE: entity_set_model(ent, gszBlockModelIce);
			case BM_DEATH: entity_set_model(ent, gszBlockModelDeath);
			case BM_NUKE: entity_set_model(ent, gszBlockModelNuke);
			case BM_CAMOUFLAGE: entity_set_model(ent, gszBlockModelCamouflage);
			case BM_LOWGRAVITY: entity_set_model(ent, gszBlockModelLowGravity);
			case BM_FIRE: entity_set_model(ent, gszBlockModelFire);
			case BM_SLAP: entity_set_model(ent, gszBlockModelSlap);
			case BM_RANDOM: entity_set_model(ent, gszBlockModelRandom);
			case BM_HONEY: entity_set_model(ent, gszBlockModelHoney);
			case BM_BARRIER_CT: entity_set_model(ent, gszBlockModelBarrierCT);
			case BM_BARRIER_T: entity_set_model(ent, gszBlockModelBarrierT);
			case BM_BOOTSOFSPEED: entity_set_model(ent, gszBlockModelBootsOfSpeed);
			default: entity_set_model(ent, gszBlockModelDefault);
		}
		
		entity_set_vector(ent, EV_VEC_angles, vAngles);
		entity_set_size(ent, vSizeMin, vSizeMax);
		entity_set_int(ent, EV_INT_body, blockType);
		
		//if a player is creating the block
		if (id > 0 && id <= 32)
		{
			//do snapping for new block
			doSnapping(id, ent, vOrigin);
		}
		
		//set origin of new block
		entity_set_origin(ent, vOrigin);
		
		//setup special properties on some blocks
		switch (blockType)
		{
			case BM_STEALTH: set_rendering(ent, kRenderFxNone, 255, 255, 255, kRenderTransAdd, 180);
			case BM_INVINCIBILITY: set_rendering(ent, kRenderFxGlowShell, 255, 255, 255, kRenderNormal, 16);
			
			case BM_RANDOM:
			{
				//set this random block to a random block!
				new randNum = random_num(0, gRandomBlocksMax - 1);
				entity_set_int(ent, EV_INT_iuser4, gRandomBlocks[randNum]);
			}
		}
		
		//if blocktype is one which requires an additional sprite
		if (blockType == BM_FIRE || blockType == BM_TRAMPOLINE || blockType == BM_SPEEDBOOST)
		{
			//add sprite on top of the block
			new sprite = create_entity(gszInfoTarget);
			
			//make sure entity was created successfully
			if (sprite)
			{
				//create angle vector and rotate it so its horizontal
				new Float:vAngles[3];
				vAngles[0] = 90.0;
				vAngles[1] = 0.0;
				vAngles[2] = 0.0;
				
				//move the sprite up onto the top of the block, adding 0.15 to prevent flickering
				vOrigin[2] += vSizeMax[2] + 0.15;
				
				//set block properties
				entity_set_string(sprite, EV_SZ_classname, gszSpriteClassname);
				entity_set_int(sprite, EV_INT_solid, SOLID_NOT);
				entity_set_int(sprite, EV_INT_movetype, MOVETYPE_NONE);
				entity_set_vector(sprite, EV_VEC_angles, vAngles);
				
				//set sprite model depending on block type
				switch (blockType)
				{
					case BM_TRAMPOLINE: entity_set_model(sprite, gszBlockSpriteTrampoline);
					case BM_SPEEDBOOST: entity_set_model(sprite, gszBlockSpriteSpeedBoost);
					case BM_FIRE: entity_set_model(sprite, gszBlockSpriteFire);
				}
				
				//set the rendermode to additive and set the transparency
				entity_set_int(sprite, EV_INT_rendermode, 5);
				entity_set_float(sprite, EV_FL_renderamt, 255.0);
				
				//set origin of new sprite
				entity_set_origin(sprite, vOrigin);
				
				//link this sprite to the block
				entity_set_int(ent, EV_INT_iuser3, sprite);
				
				//set task for animating the sprite
				if (blockType == BM_FIRE || blockType == BM_TRAMPOLINE)
				{
					new params[2];
					params[0] = sprite;
					params[1] = 8;		//both the fire and trampoline sprites have 8 frames
					set_task(0.1, "taskSpriteNextFrame", TASK_SPRITE + sprite, params, 2, "b");
				}
			}
		}
		
		return ent;
	}
	
	return 0;
}

convertBlockAiming(id, const convertTo)
{
	//make sure player has access to this command
	if (get_user_flags(id) & BM_ADMIN_LEVEL)
	{
		//get entity that player is aiming at
		new ent, body;
		get_user_aiming(id, ent, body);
		
		//if player is aiming at a block
		if (isBlock(ent))
		{
			//get who is currently grabbing the block (if anyone)
			new grabber = entity_get_int(ent, EV_INT_iuser2);
			
			//if entity is not being grabbed by someone else
			if (grabber == 0 || grabber == id)
			{
				//get the player ID of who has the block in a group (if anyone)
				new player = entity_get_int(ent, EV_INT_iuser1);
				
				//if the block is not in a group or is in this players group
				if (player == 0 || player == id)
				{
					new newBlock;
					
					//if block is in the players group and group count is larger than 1
					if (isBlockInGroup(id, ent) && gGroupCount[id] > 1)
					{
						new block;
						new blockCount = 0;
						
						//iterate through all blocks in the players group
						for (new i = 0; i <= gGroupCount[id]; ++i)
						{
							block = gGroupedBlocks[id][i];
							
							//if this block is in this players group
							if (isBlockInGroup(id, block))
							{
								//convert the block
								newBlock = convertBlock(id, block, convertTo);
								
								//if block was converted
								if (newBlock != 0)
								{
									//new block is now in the group
									gGroupedBlocks[id][i] = newBlock;
									
									//set the block so it is now 'being grouped'
									groupBlock(id, newBlock);
								}
								//count how many blocks could NOT be converted
								else
								{
									++blockCount;
								}
							}
						}
						
						//if some blocks could NOT be converted
						if (blockCount > 1)
						{
							client_print(id, print_chat, "%sCouldn't convert %d blocks!", gszPrefix, blockCount);
						}
					}
					else
					{
						newBlock = convertBlock(id, ent, convertTo);
						
						//if block was not converted
						if (newBlock == 0)
						{
							//get the block type
							new blockType = entity_get_int(ent, EV_INT_body);
							
							client_print(id, print_chat, "%sYou cannot convert a %s block into a %s block while it is rotated!", gszPrefix, gszBlockNames[blockType], gszBlockNames[convertTo]);
						}
					}
				}
				else
				{
					//get name of player who has this block in their group
					new szName[32]; 
					get_user_name(player, szName, 32);
					
					//notify player who has this block in their group
					client_print(id, print_chat, "%s%s currently has this block in their group!", gszPrefix, szName);
				}
			}
		}
	}
}

convertBlock(id, ent, const convertTo)
{
	
	new blockType = entity_get_int(ent, EV_INT_body);
	
	//if block to convert to is different to block to convert
	if (blockType != convertTo)
	{
		new Float:vOrigin[3];
		new Float:vAngles[3];
		new Float:vSizeMin[3];
		new Float:vSizeMax[3];
		
		//get block information from block player is aiming at
		entity_get_vector(ent, EV_VEC_origin, vOrigin);
		entity_get_vector(ent, EV_VEC_angles, vAngles);
		entity_get_vector(ent, EV_VEC_mins, vSizeMin);
		entity_get_vector(ent, EV_VEC_maxs, vSizeMax);
		
		//if block is rotated and we're trying to convert it to a block that cannot be rotated
		if (vAngles[0] == 90.0 && (convertTo == BM_FIRE || convertTo == BM_TRAMPOLINE || convertTo == BM_SPEEDBOOST))
		{
			return 0;
		}
		else
		{
			//delete old block and create new one of given type
			deleteBlock(ent);
			return createBlock(id, convertTo, vOrigin, vAngles, vSizeMin, vSizeMax);
		}
	}
	
	return ent;
}

deleteBlockAiming(id)
{
	//make sure player has access to this command
	if (get_user_flags(id) & BM_ADMIN_LEVEL)
	{
		//get entity that player is aiming at
		new ent, body;
		get_user_aiming(id, ent, body);
		
		//if entity player is aiming at is a block
		if (isBlock(ent))
		{
			//get who is currently grabbing the block (if anyone)
			new grabber = entity_get_int(ent, EV_INT_iuser2);
			
			//if entity is not being grabbed by someone else
			if (grabber == 0 || grabber == id)
			{
				//get the player ID of who has the block in a group (if anyone)
				new player = entity_get_int(ent, EV_INT_iuser1);
				
				//if the block is not in a group or is in this players group
				if (player == 0 || player == id)
				{
					//if block is not being grabbed
					if (entity_get_int(ent, EV_INT_iuser2) == 0)
					{
						//if block is in the players group and group count is larger than 1
						if (isBlockInGroup(id, ent) && gGroupCount[id] > 1)
						{
							new block;
							
							//iterate through all blocks in the players group
							for (new i = 0; i <= gGroupCount[id]; ++i)
							{
								block = gGroupedBlocks[id][i];
								
								//if block is still valid
								if (is_valid_ent(block))
								{
									//get player id of who has this block in their group
									new player = entity_get_int(block, EV_INT_iuser1);
									
									//if block is still in this players group
									if (player == id)
									{
										//delete the block
										deleteBlock(block);
									}
								}
							}
						}
						else
						{
							//delete the block
							deleteBlock(ent);
						}
					}
				}
				else
				{
					//get name of player who has this block in their group
					new szName[32]; 
					get_user_name(player, szName, 32);
					
					//notify player who has this block in their group
					client_print(id, print_chat, "%s%s currently has this block in their group!", gszPrefix, szName);
				}
			}
		}
	}
}

bool:deleteBlock(ent)
{
	//if entity is a block
	if (isBlock(ent))
	{
		//get the sprite attached to the top of the block
		new sprite = entity_get_int(ent, EV_INT_iuser3);
		
		//if sprite entity is valid
		if (sprite)
		{
			//remove the task for the animation of the sprite (if one exists)
			if (task_exists(TASK_SPRITE + sprite))
			{
				remove_task(TASK_SPRITE + sprite);
			}
			
			//delete the sprite
			remove_entity(sprite);
		}
		
		//delete the block
		remove_entity(ent);
		
		//block was deleted
		return true;
	}
	
	//block was not deleted
	return false;
}

rotateBlockAiming(id)
{
	//make sure player has access to this command
	if (get_user_flags(id) & BM_ADMIN_LEVEL)
	{
		//get block that player is aiming at
		new ent, body;
		get_user_aiming(id, ent, body);
		
		//if entity found is a block
		if (isBlock(ent))
		{
			//get who is currently grabbing the block (if anyone)
			new grabber = entity_get_int(ent, EV_INT_iuser2);
			
			//if entity is not being grabbed by someone else
			if (grabber == 0 || grabber == id)
			{
				//get the player ID of who has the block in a group (if anyone)
				new player = entity_get_int(ent, EV_INT_iuser1);
				
				//if the block is not in a group or is in this players group
				if (player == 0 || player == id)
				{
					//if block is in the players group and group count is larger than 1
					if (isBlockInGroup(id, ent) && gGroupCount[id] > 1)
					{
						new block;
						new bool:bRotateGroup = true;
						
						//iterate through all blocks in the players group
						for (new i = 0; i <= gGroupCount[id]; ++i)
						{
							block = gGroupedBlocks[id][i];
							
							//if block is in players group
							if (isBlockInGroup(id, block))
							{
								//get block type
								new blockType = entity_get_int(block, EV_INT_body);
								 
								//if block cannot be rotated
								if (!isBlockTypeRotatable(blockType))
								{
									//found a block that cannot be rotated
									bRotateGroup = false;
									
									break;
								}
							}
						}
						
						//if we can rotate the group
						if (bRotateGroup)
						{
							//iterate through all blocks in the players group
							for (new i = 0; i <= gGroupCount[id]; ++i)
							{
								block = gGroupedBlocks[id][i];
								
								//if block is still valid
								if (isBlockInGroup(id, block))
								{
									//rotate the block
									rotateBlock(block);
								}
							}
						}
						else
						{
							//notify player that their group cannot be rotated
							client_print(id, print_chat, "%sYour group contains blocks that cannot be rotated!", gszPrefix);
						}
					}
					else
					{
						//rotate the block and get rotated block ID
						new bool:bRotatedBlock = rotateBlock(ent);
						
						//if block did not rotate successfully
						if (!bRotatedBlock)
						{
							//get block type
							new blockType = entity_get_int(ent, EV_INT_body);
							
							//notify player block couldn't rotate
							client_print(id, print_chat, "%s%s blocks cannot be rotated!", gszPrefix, gszBlockNames[blockType]);
						}
					}
				}
				else
				{
					//get name of player who has this block in their group
					new szName[32]; 
					get_user_name(player, szName, 32);
					
					//notify player who has this block in their group
					client_print(id, print_chat, "%s%s currently has this block in their group!", gszPrefix, szName);
				}
			}
		}
	}
}

bool:rotateBlock(ent)
{
	//if entity is valid
	if (is_valid_ent(ent))
	{
		//get block type
		new blockType = entity_get_int(ent, EV_INT_body);
		
		//if block is a type that can be rotated (a block without a sprite, makes it easier!)
		if (isBlockTypeRotatable(blockType))
		{
			new Float:vAngles[3]; 
			new Float:vSizeMin[3];
			new Float:vSizeMax[3];
			
			//get block information 
			entity_get_vector(ent, EV_VEC_angles, vAngles);
			
			//create new block using current block information with new angles and sizes
			if (vAngles[0] == 0.0 && vAngles[2] == 0.0)
			{
				vAngles[0] = 90.0;
				vSizeMin = gfBlockSizeMinForX;
				vSizeMax = gfBlockSizeMaxForX;
			}
			else if (vAngles[0] == 90.0 && vAngles[2] == 0.0)
			{
				vAngles[0] = 90.0;
				vAngles[2] = 90.0;
				vSizeMin = gfBlockSizeMinForY;
				vSizeMax = gfBlockSizeMaxForY;
			}
			else
			{ 
				vAngles = gfDefaultBlockAngles;
				vSizeMin = gfBlockSizeMinForZ;
				vSizeMax = gfBlockSizeMaxForZ;
			}
			
			entity_set_vector(ent, EV_VEC_angles, vAngles);
			entity_set_size(ent, vSizeMin, vSizeMax);
			
			return true;
		}
	}
	
	return false;
}

copyBlock(ent)
{
	//if entity is valid
	if (is_valid_ent(ent))
	{
		new Float:vOrigin[3];
		new Float:vAngles[3];
		new Float:vSizeMin[3];
		new Float:vSizeMax[3];
		new blockType;
		
		//get blocktype and origin of currently grabbed block
		blockType = entity_get_int(ent, EV_INT_body);
		entity_get_vector(ent, EV_VEC_origin, vOrigin);
		entity_get_vector(ent, EV_VEC_angles, vAngles);
		entity_get_vector(ent, EV_VEC_mins, vSizeMin);
		entity_get_vector(ent, EV_VEC_maxs, vSizeMax);
		
		//create a block of the same type in the same location
		return createBlock(0, blockType, vOrigin, vAngles, vSizeMin, vSizeMax);
	}
	
	return 0;
}

/* BLOCK TESTS */
bool:isBlockInGroup(id, ent)
{
	//is entity valid
	if (is_valid_ent(ent))
	{
		//get player who has this block in their group (if anyone)
		new player = entity_get_int(ent, EV_INT_iuser1);
		
		if (player == id)
		{
			return true;
		}
	}
	
	return false;
}

bool:isBlockTypeRotatable(blockType)
{
	if (blockType != BM_FIRE && blockType != BM_TRAMPOLINE && blockType != BM_SPEEDBOOST)
	{
		return true;
	}
	
	return false;
}

bool:isBlock(ent)
{
	//is it a valid entity
	if (is_valid_ent(ent))
	{
		//get classname of entity
		new szClassname[32];
		entity_get_string(ent, EV_SZ_classname, szClassname, 32);
		
		//if classname of entity matches global block classname
		if (equal(szClassname, gszBlockClassname) || equal(szClassname, "bcm"))
		{
			//entity is a block
			return true;
		}
	}
	
	//entity is not a block
	return false;
}

bool:isBlockStuck(ent)
{
	//first make sure the entity is valid
	if (is_valid_ent(ent))
	{
		new content;
		new Float:vOrigin[3];
		new Float:vPoint[3];
		new Float:fSizeMin[3];
		new Float:fSizeMax[3];
		
		//get the size of the block being grabbed
		entity_get_vector(ent, EV_VEC_mins, fSizeMin);
		entity_get_vector(ent, EV_VEC_maxs, fSizeMax);
		
		//get the origin of the block
		entity_get_vector(ent, EV_VEC_origin, vOrigin);
		
		//decrease the size values of the block
		fSizeMin[0] += 1.0;
		fSizeMax[0] -= 1.0;
		fSizeMin[1] += 1.0;
		fSizeMax[1] -= 1.0; 
		fSizeMin[2] += 1.0;
		fSizeMax[2] -= 1.0;
		
		//get the contents of the centre of all 6 faces of the block
		for (new i = 0; i < 14; ++i)
		{
			//start by setting the point to the origin of the block (the middle)
			vPoint = vOrigin;
			
			//set the values depending on the loop number
			switch (i)
			{
				//corners
				case 0: { vPoint[0] += fSizeMax[0]; vPoint[1] += fSizeMax[1]; vPoint[2] += fSizeMax[2]; }
				case 1: { vPoint[0] += fSizeMin[0]; vPoint[1] += fSizeMax[1]; vPoint[2] += fSizeMax[2]; }
				case 2: { vPoint[0] += fSizeMax[0]; vPoint[1] += fSizeMin[1]; vPoint[2] += fSizeMax[2]; }
				case 3: { vPoint[0] += fSizeMin[0]; vPoint[1] += fSizeMin[1]; vPoint[2] += fSizeMax[2]; }
				case 4: { vPoint[0] += fSizeMax[0]; vPoint[1] += fSizeMax[1]; vPoint[2] += fSizeMin[2]; }
				case 5: { vPoint[0] += fSizeMin[0]; vPoint[1] += fSizeMax[1]; vPoint[2] += fSizeMin[2]; }
				case 6: { vPoint[0] += fSizeMax[0]; vPoint[1] += fSizeMin[1]; vPoint[2] += fSizeMin[2]; }
				case 7: { vPoint[0] += fSizeMin[0]; vPoint[1] += fSizeMin[1]; vPoint[2] += fSizeMin[2]; }
				
				//centre of faces
				case 8: { vPoint[0] += fSizeMax[0]; }
				case 9: { vPoint[0] += fSizeMin[0]; }
				case 10: { vPoint[1] += fSizeMax[1]; }
				case 11: { vPoint[1] += fSizeMin[1]; }
				case 12: { vPoint[2] += fSizeMax[2]; }
				case 13: { vPoint[2] += fSizeMin[2]; }
			}
			
			//get the contents of the point on the block
			content = point_contents(vPoint);
			
			//if the point is out in the open
			if (content == CONTENTS_EMPTY || content == 0)
			{
				//block is not stuck
				return false;
			}
		}
	}
	else
	{
		//entity is invalid but don't say its stuck
		return false;
	}
	
	//block is stuck
	return true;
}

bool:isTeleport(ent)
{
	if (is_valid_ent(ent))
	{
		//get classname of entity
		new szClassname[32];
		entity_get_string(ent, EV_SZ_classname, szClassname, 32);
		
		//compare classnames
		if (equal(szClassname, gszTeleportStartClassname) || equal(szClassname, gszTeleportEndClassname))
		{
			//entity is a teleport
			return true;
		}
	}
	
	//entity is not a teleport
	return false;
}

doSnapping(id, ent, Float:fMoveTo[3])
{
	//if player has snapping enabled
	if (gbSnapping[id])
	{
		new Float:fSnapSize = gfSnapDistance + gfSnappingGap[id];
		new Float:vReturn[3];
		new Float:dist;
		new Float:distOld = 9999.9;
		new Float:vTraceStart[3];
		new Float:vTraceEnd[3];
		new tr;
		new trClosest = 0;
		new blockFace;
		
		//get the size of the block being grabbed
		new Float:fSizeMin[3];
		new Float:fSizeMax[3];
		entity_get_vector(ent, EV_VEC_mins, fSizeMin);
		entity_get_vector(ent, EV_VEC_maxs, fSizeMax);
		
		//do 6 traces out from each face of the block
		for (new i = 0; i < 6; ++i)
		{
			//setup the start of the trace
			vTraceStart = fMoveTo;
			
			switch (i)
			{
				case 0: vTraceStart[0] += fSizeMin[0];		//edge of block on -X
				case 1: vTraceStart[0] += fSizeMax[0];		//edge of block on +X
				case 2: vTraceStart[1] += fSizeMin[1];		//edge of block on -Y
				case 3: vTraceStart[1] += fSizeMax[1];		//edge of block on +Y
				case 4: vTraceStart[2] += fSizeMin[2];		//edge of block on -Z
				case 5: vTraceStart[2] += fSizeMax[2];		//edge of block on +Z
			}
			
			//setup the end of the trace
			vTraceEnd = vTraceStart;
			
			switch (i)
			{
				case 0: vTraceEnd[0] -= fSnapSize;
				case 1: vTraceEnd[0] += fSnapSize;
				case 2: vTraceEnd[1] -= fSnapSize;
				case 3: vTraceEnd[1] += fSnapSize;
				case 4: vTraceEnd[2] -= fSnapSize;
				case 5: vTraceEnd[2] += fSnapSize;
			}
			
			//trace a line out from one of the block faces
			tr = trace_line(ent, vTraceStart, vTraceEnd, vReturn);
			
			//if the trace found a block and block is not in group or block to snap to is not in group
			if (isBlock(tr) && (!isBlockInGroup(id, tr) || !isBlockInGroup(id, ent)))
			{
				//get the distance from the grabbed block to the found block
				dist = get_distance_f(vTraceStart, vReturn);
				
				//if distance to found block is less than the previous block
				if (dist < distOld)
				{
					trClosest = tr;
					distOld = dist;
					
					//save the block face where the trace came from
					blockFace = i;
				}
			}
		}
		
		//if there is a block within the snapping range
		if (is_valid_ent(trClosest))
		{
			//get origin of closest block
			new Float:vOrigin[3];
			entity_get_vector(trClosest, EV_VEC_origin, vOrigin);
			
			//get sizes of closest block
			new Float:fTrSizeMin[3];
			new Float:fTrSizeMax[3];
			entity_get_vector(trClosest, EV_VEC_mins, fTrSizeMin);
			entity_get_vector(trClosest, EV_VEC_maxs, fTrSizeMax);
			
			//move the subject block to the origin of the closest block
			fMoveTo = vOrigin;
			
			//offset the block to be on the side where the trace hit the closest block
			if (blockFace == 0) fMoveTo[0] += (fTrSizeMax[0] + fSizeMax[0]) + gfSnappingGap[id];
			if (blockFace == 1) fMoveTo[0] += (fTrSizeMin[0] + fSizeMin[0]) - gfSnappingGap[id];
			if (blockFace == 2) fMoveTo[1] += (fTrSizeMax[1] + fSizeMax[1]) + gfSnappingGap[id];
			if (blockFace == 3) fMoveTo[1] += (fTrSizeMin[1] + fSizeMin[1]) - gfSnappingGap[id];
			if (blockFace == 4) fMoveTo[2] += (fTrSizeMax[2] + fSizeMax[2]) + gfSnappingGap[id];
			if (blockFace == 5) fMoveTo[2] += (fTrSizeMin[2] + fSizeMin[2]) - gfSnappingGap[id];
		}
	}
}

/***** FILE HANDLING *****/
saveBlocks(id)
{
	//make sure player has access to this command
	if (get_user_flags(id) & BM_ADMIN_LEVEL)
	{
		new file = fopen(gszNewFile, "wt");
		new ent = -1;
		new blockType;
		new Float:vOrigin[3];
		new Float:vAngles[3];
		new Float:vStart[3];
		new Float:vEnd[3];
		new blockCount = 0;
		new teleCount = 0;
		new szData[128];
		
		while ((ent = find_ent_by_class(ent, gszBlockClassname)))
		{
			//get block info
			blockType = entity_get_int(ent, EV_INT_body);
			entity_get_vector(ent, EV_VEC_origin, vOrigin);
			entity_get_vector(ent, EV_VEC_angles, vAngles);
			
			//format block info and save it to file
			formatex(szData, 128, "%c %f %f %f %f %f %f^n", gBlockSaveIds[blockType], vOrigin[0], vOrigin[1], vOrigin[2], vAngles[0], vAngles[1], vAngles[2]);
			fputs(file, szData);
			
			//increment block count
			++blockCount;
		}
		
		//iterate through teleport end entities because you can't have an end without a start
		ent = -1;
		
		while ((ent = find_ent_by_class(ent, gszTeleportEndClassname)))
		{
			//get the id of the start of the teleporter
			new tele = entity_get_int(ent, EV_INT_iuser1);
			
			//check that start of teleport is a valid entity
			if (tele)
			{
				//get the origin of the start of the teleport and save it to file
				entity_get_vector(tele, EV_VEC_origin, vStart);
				entity_get_vector(ent, EV_VEC_origin, vEnd);
				
				formatex(szData, 128, "%s %f %f %f %f %f %f^n", "*", vStart[0], vStart[1], vStart[2], vEnd[0], vEnd[1], vEnd[2]);
				fputs(file, szData);
				
				//2 teleport entities count as 1 teleporter
				++teleCount;
			}
		}
		
		//get players name
		new szName[32];
		get_user_name(id, szName, 32);
		
		//notify all admins that the player saved blocks to file
		for (new i = 1; i <= 32; ++i)
		{
			//make sure player is connected
			if (is_user_connected(i))
			{
				if (get_user_flags(i) & BM_ADMIN_LEVEL)
				{
					client_print(i, print_chat, "%s'%s' saved %d block%s and %d teleporter%s to file! Total entites in map: %d", gszPrefix, szName, blockCount, (blockCount == 1 ? "" : "s"), teleCount, (teleCount == 1 ? "" : "s"), entity_count());
				}
			}
		}
		
		//close file
		fclose(file);
	}
}

loadBlocks(id)
{
	new bool:bAccess = false;
	
	//if this function was called on map load, ID is 0
	if (id == 0)
	{
		bAccess = true;
	}
	//make sure user calling this function has access
	else if (get_user_flags(id) & BM_ADMIN_LEVEL)
	{
		bAccess = true;
	}
	
	if (bAccess)
	{
		//if map file exists
		if (file_exists(gszNewFile))
		{
			//if a player is loading the blocks then first delete all the old blocks and teleports
			if (id > 0 && id <= 32)
			{
				deleteAllBlocks(id, false);
				deleteAllTeleports(id, false);
			}
			
			new szData[128];
			new szType[2];
			new sz1[16], sz2[16], sz3[16], sz4[16], sz5[16], sz6[16];
			new Float:vVec1[3];
			new Float:vVec2[3];
			new Float:fSizeMin[3];
			new Float:fSizeMax[3];
			new f = fopen(gszNewFile, "rt");
			new blockCount = 0;
			new teleCount = 0;
			
			//iterate through all the lines in the file
			while (!feof(f))
			{
				szType = "";
				fgets(f, szData, 128);
				parse(szData, szType, 1, sz1, 16, sz2, 16, sz3, 16, sz4, 16, sz5, 16, sz6, 16);
				
				vVec1[0] = str_to_float(sz1);
				vVec1[1] = str_to_float(sz2);
				vVec1[2] = str_to_float(sz3);
				vVec2[0] = str_to_float(sz4);
				vVec2[1] = str_to_float(sz5);
				vVec2[2] = str_to_float(sz6);
				
				if (strlen(szType) > 0)
				{
					//if type is not a teleport
					if (szType[0] != '*')
					{
						//set block size depending on block angles
						if (vVec2[0] == 90.0 && vVec2[1] == 0.0 && vVec2[2] == 0.0)
						{
							fSizeMin = gfBlockSizeMinForX;
							fSizeMax = gfBlockSizeMaxForX;
						}
						else if (vVec2[0] == 90.0 && vVec2[1] == 0.0 && vVec2[2] == 90.0)
						{
							fSizeMin = gfBlockSizeMinForY;
							fSizeMax = gfBlockSizeMaxForY;
						}
						else
						{
							fSizeMin = gfBlockSizeMinForZ;
							fSizeMax = gfBlockSizeMaxForZ;
						}
						
						//increment block counter
						++blockCount;
					}
					
					//create block or teleport depending on type
					switch (szType[0])
					{
						case 'A': createBlock(0, BM_PLATFORM, vVec1, vVec2, fSizeMin, fSizeMax);
						case 'B': createBlock(0, BM_BHOP, vVec1, vVec2, fSizeMin, fSizeMax);
						case 'C': createBlock(0, BM_DAMAGE, vVec1, vVec2, fSizeMin, fSizeMax);
						case 'D': createBlock(0, BM_HEALER, vVec1, vVec2, fSizeMin, fSizeMax);
						case 'E': createBlock(0, BM_INVINCIBILITY, vVec1, vVec2, fSizeMin, fSizeMax);
						case 'F': createBlock(0, BM_STEALTH, vVec1, vVec2, fSizeMin, fSizeMax);
						case 'G': createBlock(0, BM_TRAMPOLINE, vVec1, vVec2, fSizeMin, fSizeMax);
						case 'H': createBlock(0, BM_SPEEDBOOST, vVec1, vVec2, fSizeMin, fSizeMax);
						case 'I': createBlock(0, BM_NOFALLDAMAGE, vVec1, vVec2, fSizeMin, fSizeMax);
						case 'J': createBlock(0, BM_ICE, vVec1, vVec2, fSizeMin, fSizeMax);
						case 'K': createBlock(0, BM_DEATH, vVec1, vVec2, fSizeMin, fSizeMax);
						case 'L': createBlock(0, BM_NUKE, vVec1, vVec2, fSizeMin, fSizeMax);
						case 'M': createBlock(0, BM_CAMOUFLAGE, vVec1, vVec2, fSizeMin, fSizeMax);
						case 'N': createBlock(0, BM_LOWGRAVITY, vVec1, vVec2, fSizeMin, fSizeMax);
						case 'O': createBlock(0, BM_FIRE, vVec1, vVec2, fSizeMin, fSizeMax);
						case 'P': createBlock(0, BM_SLAP, vVec1, vVec2, fSizeMin, fSizeMax);
						case 'Q': createBlock(0, BM_RANDOM, vVec1, vVec2, fSizeMin, fSizeMax);
						case 'R': createBlock(0, BM_HONEY, vVec1, vVec2, fSizeMin, fSizeMax);
						case 'S': createBlock(0, BM_BARRIER_CT, vVec1, vVec2, fSizeMin, fSizeMax);
						case 'T': createBlock(0, BM_BARRIER_T, vVec1, vVec2, fSizeMin, fSizeMax);
						case 'U': createBlock(0, BM_BOOTSOFSPEED, vVec1, vVec2, fSizeMin, fSizeMax);
						
						case '*':
						{
							createTeleport(0, TELEPORT_START, vVec1);
							createTeleport(0, TELEPORT_END, vVec2);
							
							//increment teleport count
							++teleCount;
						}
						
						default:
						{
							log_amx("%sInvalid block type: %c in: %s", gszPrefix, szType[0], gszFile);
							
							//decrement block counter because a block was not created
							--blockCount;
						}
					}
				}
			}
			
			fclose(f);
			
			//if a player is loading the blocks
			if (id > 0 && id <= 32)
			{
				//get players name
				new szName[32];
				get_user_name(id, szName, 32);
				
				//notify all admins that the player loaded blocks from file
				for (new i = 1; i <= 32; ++i)
				{
					//make sure player is connected
					if (is_user_connected(i))
					{
						if (get_user_flags(i) & BM_ADMIN_LEVEL)
						{
							client_print(i, print_chat, "%s'%s' loaded %d block%s and %d teleporter%s from file! Total entites in map: %d", gszPrefix, szName, blockCount, (blockCount == 1 ? "" : "s"), teleCount, (teleCount == 1 ? "" : "s"), entity_count());
						}
					}
				}
			}
		}
		else
		{
			//if a player is loading the blocks
			if (id > 0 && id <= 32)
			{
				//notify player that the file could not be found
				client_print(id, print_chat, "%sCouldn't find file: %s", gszPrefix, gszNewFile);
			}
			else
			{
				log_amx("%sCant find blocks file. Trying old save/load method...", gszPrefix);
			}
			
			//check old saving/loading method
			loadBlocksOld();
		}
	}
}

loadBlocksOld()
{
	if (!file_exists(gszFile))
	{
		return;
	}
	
	new szData[128];
	new szType[2];
	new oX[13], oY[13], oZ[13];
	new aX[13], aY[13], aZ[13];
	new Float:vOrigin[3];
	new Float:vAngles[3];
	new f = fopen(gszFile, "rt");
	new blockType;
	
	//iterate through all the lines in the file
	while (!feof(f))
	{
		szType = "";
		fgets(f, szData, 128);
		parse(szData, szType, 2, oX, 12, oY, 12, oZ, 12, aX, 12, aY, 12, aZ, 12);
		
		vOrigin[0] = str_to_float(oX);
		vOrigin[1] = str_to_float(oY);
		vOrigin[2] = str_to_float(oZ);
		vAngles[0] = str_to_float(aX);
		vAngles[1] = str_to_float(aY);
		vAngles[2] = str_to_float(aZ);
		
		blockType = -1;
		
		if (strlen(szType) > 0)
		{
			//first check if type is a teleporter
			if (szType[0] == 'S')
			{
				createTeleport(0, TELEPORT_START, vOrigin);
			}
			else if (szType[0] == 'D')
			{
				createTeleport(0, TELEPORT_END, vOrigin);
			}
			else
			{
				blockType = str_to_num(szType);
				
				//if blockType number is valid, create the block
				if (blockType >= 0 && blockType < gBlockMax)
				{
					//create clipping vectors from angles
					if (vAngles[0] == 90.0 && vAngles[1] == 0.0 && vAngles[2] == 0.0)
					{
						createBlock(0, blockType, vOrigin, vAngles, gfBlockSizeMinForX, gfBlockSizeMaxForX);
					}
					else if (vAngles[0] == 90.0 && vAngles[1] == 0.0 && vAngles[2] == 90.0)
					{
						createBlock(0, blockType, vOrigin, vAngles, gfBlockSizeMinForY, gfBlockSizeMaxForY);
					}
					else
					{
						createBlock(0, blockType, vOrigin, gfDefaultBlockAngles, gfBlockSizeMinForZ, gfBlockSizeMaxForZ);
					}
				}
				else
				{
					log_amx("%sInvalid box type: %c in: %s", gszPrefix, szType[0], gszFile);
				}
			}
		}
	}
	
	fclose(f);
}

/* MISC */
drawLine(Float:vOrigin1[3], Float:vOrigin2[3], life)
{
	message_begin(MSG_BROADCAST, SVC_TEMPENTITY);
	write_byte(TE_BEAMPOINTS);
	write_coord(floatround(vOrigin1[0], floatround_floor));
	write_coord(floatround(vOrigin1[1], floatround_floor));
	write_coord(floatround(vOrigin1[2], floatround_floor));
	write_coord(floatround(vOrigin2[0], floatround_floor));
	write_coord(floatround(vOrigin2[1], floatround_floor));
	write_coord(floatround(vOrigin2[2], floatround_floor));
	write_short(gSpriteIdBeam);		//sprite index
	write_byte(0);				//starting frame
	write_byte(1);				//frame rate in 0.1's
	write_byte(life);			//life in 0.1's
	write_byte(5);				//line width in 0.1's
	write_byte(0);				//noise amplitude in 0.01's
	write_byte(255);			//red
	write_byte(255);			//green
	write_byte(255);			//blue
	write_byte(255);			//brightness
	write_byte(0);				//scroll speed in 0.1's
	message_end();
}
