/********************************************************************************
*  	AMX Mod X Script.
*
*   Speed Hack Detector
*   Formatright (C) 2009 OT
*
*   This program is free software; you can redistribute it and/or
*   modify it under the terms of the GNU General Public License
*   as published by the Free Software Foundation; either version 2
*   of the License, or (at your option) any later version.
*
*   This program is distributed in the hope that it will be useful,
*   but WITHOUT ANY WARRANTY; without even the implied warranty of
*   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
*   GNU General Public License for more details.
*
*   You should have received a copy of the GNU General Public License
*   along with this program; if not, write to the Free Software
*   Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
*
*   In addition, as a special exception, the author gives permission to
*   link the code of this program with the Half-Life Game Engine ("HL
*   Engine") and Modified Game Libraries ("MODs") developed by Valve,
*   L.L.C ("Valve"). You must obey the GNU General Public License in all
*   respects for all of the code used other than the HL Engine and MODs
*   from Valve. If you modify this file, you may extend this exception
*   to your version of the file, but you are not obligated to do so. If
*   you do not wish to do so, delete this exception statement from your
*   version.
*
**********************************************************************************/

/*  [Plugin Link]
	http://forums.alliedmods.net/showthread.php?t=131219
*/

/* [Changelog]
- 1.4  - added developer cvar kick
- 1.3  - improved logging, increaced tolerance
- 1.2  - added more cvars for customization 
- 1.1  - added one more option to punishtype cvar 
- 1.0  - initial release 
*/

#include <amxmodx>
#include <fakemeta>
#include <hamsandwich>

#define add_bot_property(%1)						gBS_cl_bot |= (1<<(%1 - 1))
#define del_bot_property(%1)						gBS_cl_bot &= ~(1<<(%1 - 1))
#define has_bot_property(%1)						(gBS_cl_bot & (1<<(%1 - 1)))
#define add_alive_property(%1)						gBS_cl_alive |= (1<<(%1 - 1))
#define del_alive_property(%1)						gBS_cl_alive &= ~(1<<(%1 - 1))
#define has_alive_property(%1)						(gBS_cl_alive & (1<<(%1 - 1)))
#define add_announced(%1)							gBS_cl_announce |= (1<<(%1 - 1))
#define del_announced(%1)							gBS_cl_announce &= ~(1<<(%1 - 1))
#define has_been_announced(%1)						(gBS_cl_announce & (1<<(%1 - 1)))

const gC_MaxIdle =  2500
const gC_MaxLimit = 130
const gC_MaxSlots = 32

new gBS_cl_alive, gBS_cl_bot, gBS_cl_announce
new gPV_punishtype, gPV_enable, gCV_enable, gPV_bantime, gPV_adminonly, gPV_logbans

new gCLI_count[gC_MaxSlots + 1]
new gCLI_buttons[gC_MaxSlots + 1]
new gCLI_idlecount[gC_MaxSlots + 1] 
new Float:gCLF_lasttime[gC_MaxSlots + 1]
new Float:gCLV_views[gC_MaxSlots + 1][3]
new Float:gCLV_pviews[gC_MaxSlots + 1][3]

public plugin_init()
{
	register_plugin("Speed Hack Detector", "1.4", "OT")
	
	register_cvar("spd_version", "1.4", FCVAR_SERVER | FCVAR_SPONLY)

	gPV_enable 		= register_cvar("spd_enable", 	  "1")
	gPV_punishtype  = register_cvar("spd_punishtype", "0")
	gPV_bantime		= register_cvar("spd_bantime", 	  "0")
	gPV_adminonly	= register_cvar("spd_adminonly",  "1")
	gPV_logbans		= register_cvar("spd_logbans",    "1")
	
	RegisterHam(Ham_Spawn, "player", "pfw_PlayerHandleAD", 1)
	RegisterHam(Ham_Killed, "player", "pfw_PlayerHandleAD", 1)
	
	register_forward(FM_CmdStart, "pfw_CmdStart", 1)
	
	set_task(1.0, "tsk_ChacheCvars", 0, "", 0, "b", 0)
}

public client_putinserver(id)
{
	del_announced(id)
	del_alive_property(id)
	
	if (is_user_bot(id))
		add_bot_property(id)
	else
		del_bot_property(id)
}

public client_disconnect(id)
{
	del_announced(id)
	del_alive_property(id)
	del_bot_property(id)
}

public tsk_ChacheCvars(id)
{
	// The !! means that we want or 0 or 1 not 34183 values
	gCV_enable = 	!!get_pcvar_num(gPV_enable)
	
	return PLUGIN_CONTINUE
}

public pfw_CmdStart(id, pUC, seed)
{
	if (!gCV_enable)
		return FMRES_IGNORED
	
	if (!has_alive_property(id) || has_bot_property(id))
		return FMRES_IGNORED
	
	new Float:fGameTime, iButtons
	new Float:vView[3]
	
	pev(id, pev_v_angle, vView)
	fGameTime = get_gametime()
	get_uc(pUC, UC_Buttons, iButtons)
	
	if (gCLI_count[id] > 0)
	{
		if (gCLI_buttons[id] == iButtons)
		{
			if (gCLV_pviews[id][0] == vView[0] && gCLV_pviews[id][1] == vView[1] && gCLV_pviews[id][2] == vView[2])
				gCLI_idlecount[id]++
			else
				gCLI_idlecount[id] = 0
			
			if (gCLI_idlecount[id] > gC_MaxIdle)
			{
				gCLV_views[id][0] == -8000.0
				gCLI_idlecount[id] = 0
			}
		}
		else
		{
			gCLI_idlecount[id] = 0
			gCLI_buttons[id] = iButtons
		}
		
		gCLV_pviews[id][0] = vView[0]
		gCLV_pviews[id][1] = vView[1]
		gCLV_pviews[id][2] = vView[2]
	}
	
	if (gCLI_count[id] < 0)
	{
		gCLI_idlecount[id] = 0
		gCLI_buttons[id] = iButtons
		gCLV_pviews[id][0] = vView[0]
		gCLV_pviews[id][1] = vView[1]
		gCLV_pviews[id][2] = vView[2]
	}
	
	if (gCLV_views[id][0] == -8000.0 && gCLI_count[id] != -9)
	{
		gCLI_count[id] = -9
		return FMRES_IGNORED
	}
	
	if (gCLI_count[id] < -2)
	{
		gCLI_buttons[id] =  iButtons
		gCLV_views[id][0] = vView[0]
		gCLV_views[id][1] = vView[1]
		gCLV_views[id][2] = vView[2]
		gCLI_count[id]++
		return FMRES_IGNORED
	}
	
	// If the user has begun targeting players
	if (gCLV_views[id][0] == vView[0] && gCLV_views[id][1] == vView[1] && gCLV_views[id][2] == vView[2])
	{
		gCLI_count[id] = -2
		return FMRES_IGNORED
	}
	
	if (fGameTime - gCLF_lasttime[id] > 1.0)
	{	
		if (gCLI_count[id] > gC_MaxLimit)
			punish_player(id)
		
		if (gCLI_count[id] < 0)
			gCLI_count[id] ++
		else
			gCLI_count[id] = 0
		
		gCLF_lasttime[id] = fGameTime
		
		return FMRES_IGNORED
	}
	
	if (gCLI_count[id] >= 0)
	{
		gCLI_count[id]++
		
		if (gCLI_count[id] % 10 == 0)
		{
			query_client_cvar(id, "developer", "qcv_developer")
		}
	}
	
	return FMRES_IGNORED
}

public pfw_PlayerHandleAD(id)
{
	if (is_user_alive(id))
	{
		if (!has_been_announced(id))
		{
			query_client_cvar(id, "developer", "qcv_developer")
			add_announced(id)
			
			if (gCV_enable)
				client_print(id, print_chat, "This server is using Speed Hack Detector made by OT v1.2.")
		}
		
		gCLF_lasttime[id] = get_gametime()
		gCLV_views[id][0] = -8000.0
		gCLI_idlecount[id] = 0
		
		add_alive_property(id)
	}
	else
		del_alive_property(id)
	
	return HAM_IGNORED
}

public qcv_developer(id, const cvar[], const value[])
{
	if ((!!str_to_num(value)) == true)
	{
		kick_user(id, "Kicked by Speed Hack Detector", "Reason: The developer cvar is set to 1", "To connect set developer to 0 and leave it that way or else you will be banned.")
	}
}

punish_player(id)
{
	new name[32]
	get_user_name(id, name, 31)

	new steamid[32]
	get_user_authid(id, steamid, 31)
	
	new ping, loss
	get_user_ping(id, ping, loss)

	switch (get_pcvar_num(gPV_punishtype))
	{
		case 1: // AMX BANS support
		{
			server_cmd("amx_ban %d #%d ^"Speed Hack^"", abs(get_pcvar_num(gPV_bantime)), get_user_userid(id))
		}
		
		case 2: // The normal ban included in admincmd.sma
		{
			server_cmd("amx_ban #%d %d ^"Speed Hack^"", get_user_userid(id), abs(get_pcvar_num(gPV_bantime)))
		}

		case 3: // The same as previous, but without extern plugins need.
		{
			server_cmd("kick #%d ^"Speed Hack (Banned permanently)^";wait;banid ^"%d.0^" ^"%s^";wait;writeid", get_user_userid(id), abs(get_pcvar_num(gPV_bantime)), steamid)
		}

		case 4: // IP ban, included in admincmd.sma
		{
			server_cmd("amx_banip #%d %d ^"Speed Hack^"", get_user_userid(id), abs(get_pcvar_num(gPV_bantime)))
		}
		
		default:
		{
			if (get_pcvar_num(gPV_adminonly))
				print_toadmin(name)
			else
				client_print(0, print_chat, "[SHD] Player %s was detected using speed hack.", name)
			
			return 1
		}
	}

	
	new message[200]
	
	formatex(message, charsmax(message), "[SHD] Name: %s | Steam ID: %s | Counts: %d | Ping: %d | Loss: %d", name, steamid, gCLI_count[id], ping, loss)

	if (get_pcvar_num(gPV_logbans))
		log_to_file("shd_bans.log", message)
	
	console_print(0, message)

	if (get_pcvar_num(gPV_adminonly))
		print_toadmin(name)
	else
		client_print(0, print_chat, "[SHD] Player %s", name, (0 < get_pcvar_num(gPV_punishtype) <= 4) ? "was banned because of the speed hack": "has been detected using speed hack")
	
	return 1
}

print_toadmin(name[])
{
	new players[32], num, id
	get_players(players, num)
	
	for (new i=0;i<num;i++)
	{
		id = players[i]
		
		if (get_user_flags(id) & ADMIN_CHAT)
			client_print(id, print_chat, "[SHD][Admin] Player %s", name, (0 < get_pcvar_num(gPV_punishtype) <= 4) ? "was banned because of the speed hack": "has been detected using speed hack")
	}
}

kick_user(id, line1[] = "", line2[] = "", line3[] = "") 
{
    new msg_content[1024], pl_name[32], pl_userid, pl_authid[35]
    
    /* grab logging infos */
    pl_userid = get_user_userid(id)
    get_user_name(id, pl_name, 31)
    get_user_authid(id, pl_authid, 34)
    
    /* do kick the player */
    format(msg_content, 1023, "%s^n%s^n%s", line1, line2, line3)
    message_begin(MSG_ONE, SVC_DISCONNECT, {0,0,0}, id)
    write_string(msg_content)
    message_end()
    
    /* log the kick as <kick> command do */
    log_message("Kick_ML: ^"%s<%d><%s><>^" was kicked by ^"Console^" (message ^"%s^" ^"%s^" ^"%s^")", pl_name, pl_userid, pl_authid, line1, line2, line3)

    return 1
}