/*
*	Spray Exploit Fixer
*	Copyright (C) 2024 Silvers
*
*	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 3 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, see <https://www.gnu.org/licenses/>.
*/



#define PLUGIN_VERSION 		"2.23"

/*=======================================================================================
	Plugin Info:

*	Name	:	[ANY] Spray Exploit Fixer
*	Author	:	SilverShot
*	Descrp	:	Deletes bad sprays and prevents them from crashing clients.
*	Link	:	https://forums.alliedmods.net/showthread.php?t=323447
*	Plugins	:	https://sourcemod.net/plugins.php?exact=exact&sortby=title&search=1&author=Silvers

========================================================================================
	Change Log:

2.23 (05-Nov-2024) - Update by ".Rushaway"
	- Added cvar "spray_exploit_fixer_punish" to specify which exploits to test for.
	- Added cvar "spray_exploit_fixer_bantime" to set the ban length.
	- Now less checks of GetClientAuthId by storing them.
	- Switch to Steam3 format for AuthID.
	- Fixed g_smWaiting not removing data for unverified clients.
	- Prevent g_smWaiting not removing data if client was already disconnected.
	- LogAction now print infos even if client is not verified.

2.22 (28-Jan-2024)
	- Fixed memory leak caused by clearing StringMap/ArrayList data instead of deleting.

2.21 (19-Feb-2023)
	- Now prevents even more log spamming duplicate entries. Thanks to ".Rushaway" for reporting.

2.20 (20-Jan-2023)
	- Now logs if a Steam ID is unverified.
	- Now prevents log spamming duplicate entries.
	- Fixed checking bots for sprays.
	- Thanks to ".Rushaway" for reporting and help testing.

2.19 (07-Jan-2023)
	- Fixed processing getting stuck. Thanks to "SuperConker" for reporting and help testing.
	- Fixed invalid handle errors. Thanks to "nikooo777" for reporting.

2.18 (24-Dec-2022)
	- Changed moving sprays to use an asynchronous method to prevent a script execution timed out error. Thanks to ".Rushaway" for reporting.

2.17 (08-Oct-2022)
	- Fixed command "sm_spray_test" getting stuck processing under certain conditions.
	- Re-wrote the recursive directory function to avoid several bugs under several conditions.
	- Now only moves sprays (.dat or .dat.ztmp) and not other files to the "backup_sprays" folder.
	- Now deletes empty directories on plugin start after moving sprays to the "backup_sprays" folder.

2.16 (30-Sep-2022)
	- Fixed not moving all sprays on disconnect.
	- Fixed client not in game errors when renaming sprays.
	- Now moves "dat.ztmp" spray files to backup folder.

2.15 (22-Sep-2022)
	- Fixed not deleting the old backup if the names match.

2.14 (22-Sep-2022)
	- Added cvar "spray_exploit_fixer_msg" to control if messages should print to the server console. Requested by ".Rushaway".
	- Plugin now moves all sprays to the "download/backup_sprays" folder on plugin start and client disconnect.
	- Removed saving checked and blocked sprays to file. All sprays will be checked.

2.13 (22-May-2022)
	- More detailed "LogAction" when kicking or banning clients.

2.12 (22-May-2022)
	- Added some more "LogAction" when kicking or banning clients.

2.11 (22-May-2022)
	- Added cvar "spray_exploit_fixer_kick" to kick clients. Ban cvar overrides this. Requested by ".Rushaway".
	- Changes to fix not kicking or banning clients under some conditions.

2.10 (23-Apr-2022)
	- Fixed the plugin blocking sprays on some servers. Thanks to "SuperConker" for reporting and lots of testing.

2.9 (10-Apr-2022)
	- Fixed showing the wrong invalid files count. Thanks to "sappho" for reporting.

2.8 (20-Mar-2022)
	- Added another check and prevention against crash exploits. Thanks to "Sreaper" and "ficool2" for lots of help.
	- Fixed some false positives due to recent updates.

2.7 (08-Mar-2022)
	- Added support for banning using the "Material Admin" plugin. Thanks to "lechuga" for adding.

2.6 (01-Mar-2022)
	- Another crash exploit fixed. Thanks to Kenzzer for reporting.

2.5 (15-Jan-2022)
	- Fixed randomly using recursive folder and extension names in spray filenames causing validation failure. Thanks to "A1m" for reporting.

2.4 (02-Dec-2021)
	- Added support for banning using the "SourceBans" plugin. Thanks to "lechuga" for adding.

2.3 (12-Nov-2021)
	- Added a check for missing downloads folder and filename. Thanks to "nebsun" for reporting.
	- Changes to fix warnings when compiling on SourceMod 1.11.

2.2 (30-Jun-2021)
	- Fixed another Spray exploit. Thanks to "Madness (null138)" for fixing and reporting.

2.1 (31-Mar-2021)
	- Added a check for "sm_sprays_allowed" in the command admin_overrides.cfg to only allow specific flag groups to use sprays.

2.0 (09-Aug-2020)
	- Now should support all games.
	- Added more checks for invalid files.
	- Added cvar "spray_exploit_fixer_path" to specify the downloads folder if not correctly detected.
	- Removed gamedata and DHooks dependency.
	- Removed cvar "spray_exploit_fixer_name".

1.6 (15-Jul-2020)
	- Fixed issue with CSS game. Thanks to "NeonC" for reporting.
	- Added cvar "spray_exploit_fixer_name" to choose the method for retrieving the spray owner.

1.5 (14-May-2020)
	- Added better error log message when gamedata file is missing.
	- Fixed gamedata for HL2:DM. Thanks to "CliptonHeist" for reporting and "asherkin" for explaining engine != game.
	- (Info: the gamedata "engine" key for HL2:DM uses "hl2dm" (the engine name) while the "game" part uses "hl2mp" (game name) e.g. for offsets).

1.4 (10-May-2020)
	- Added support for "Zombie Panic! Source" game. Requires gamedata update.
	- Fixed "sm_spray_test" timing out when checking many sprays. Thanks to "Sreaper" for reporting and testing.
	- Now checks 50 files and waits 0.1 seconds before checking the next batch.
	- TF2 updated to fix clients crashing, but this plugin is still recommended to delete the other randomly uploaded user files.

1.3 (26-Apr-2020)
	- Changed cvar "spray_exploit_fixer_log" to log all files or only invalid sprays.
	- Logging now saves to "sourcemod/logs/spray_downloads.log" file.

1.2 (23-Apr-2020)
	- Added better checks to detect more bad sprays.
	- Added better checks for TF2 and other games to avoid false positives.
	- Prevented banning people in TF2 since many random invalid files are sent, not just sprays.

1.1 (21-Apr-2020)
	- Added better checks to prevent false positives.
	- Added ability to detect the users uploading sprays or other files.
	- Added cvar "spray_exploit_fixer_ban" to ban players with invalid sprays.
	- Added cvar "spray_exploit_fixer_log" to log players and files they uploaded.
	- Changed "sm_spray_test" to allow recursive searching the downloads directory.
	- Fixed plugin crashing TF2.
	- Updated GameData required.

1.0 (20-Apr-2020)
	- Initial release.

======================================================================================*/

#pragma semicolon 1

#undef REQUIRE_PLUGIN
#tryinclude <materialadmin>
#tryinclude <sourcebanspp>
#define REQUIRE_PLUGIN

#pragma newdecls required

#include <sourcemod>
#include <sdktools>


#define MAX_READ		50
#define TIMEOUT_LOG		10.0
#define PATH_BACKUP		"backup_sprays"

int g_iVal[] = {86,84,70,0,7,0,0,0,42,0,0,0,42,0,0,0,42,42,42,42,42,42,42,42,42,42,42,0,0,0,0,0,0,0,0,0};
char g_sFilename[PLATFORM_MAX_PATH];
char g_sMoveFiles[PLATFORM_MAX_PATH];
char g_sDownloads[PLATFORM_MAX_PATH];
char g_sPath1[MAXPLAYERS+1][PLATFORM_MAX_PATH];
char g_sPath2[MAXPLAYERS+1][PLATFORM_MAX_PATH];
char g_sAuth[MAXPLAYERS+1][64];
char g_sAuthUnverified[MAXPLAYERS+1][64];
float g_fSprayed[MAXPLAYERS+1];
ConVar g_hCvarBan, g_hCvarBanTime, g_hCvarKick, g_hCvarLog, g_hCvarMsg, g_hCvarPath, g_hCvarPunish;
EngineVersion g_iEngine;
StringMap g_smChecked;
StringMap g_smReceive;
StringMap g_smWaiting;
int g_iTotal;
float g_fTime;
bool g_bLate;
bool g_bProc;
bool g_bDecal;
bool g_bSourceBans;
bool g_bMaterialAdmin;



// Added this here so it compiles on the forum without the SourceBans/MaterialAdmin includes.
#if !defined _sourcebanspp_included
native void SBPP_BanPlayer(int iAdmin, int iTarget, int iTime, const char[] sReason);
#endif
#if !defined _materialadmin_included
native bool MABanPlayer(int iClient, int iTarget, int iType, int iTime, char[] sReason);
#define MA_BAN_STEAM		1
#endif



public Plugin myinfo =
{
	name = "[ANY] Spray Exploit Fixer",
	author = "SilverShot",
	description = "Deletes bad sprays and prevents them from crashing clients.",
	version = PLUGIN_VERSION,
	url = "https://forums.alliedmods.net/showthread.php?t=323447"
}

public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max)
{
	MarkNativeAsOptional("SBPP_BanPlayer");
	MarkNativeAsOptional("MABanPlayer");

	g_iEngine = GetEngineVersion();
	g_bLate = late;

	return APLRes_Success;
}

public void OnLibraryAdded(const char []name)
{
	if( strcmp(name, "sourcebans++") == 0 )
		g_bSourceBans = true;
	else if( strcmp(name, "materialadmin") == 0 )
		g_bMaterialAdmin = true;
}

public void OnLibraryRemoved(const char []name)
{
	if( strcmp(name, "sourcebans++") == 0 )
		g_bSourceBans = false;
	else if( strcmp(name, "materialadmin") == 0 )
		g_bMaterialAdmin = false;
}

public void OnPluginStart()
{
	RegAdminCmd("sm_spray_test", CmdSprays, ADMFLAG_ROOT, "Tests all sprays in the games downloads folder, listing bad ones.");

	switch( g_iEngine )
	{
		case Engine_SourceSDK2006, Engine_SourceSDK2007, Engine_Left4Dead, Engine_Left4Dead2:
		{
			g_sDownloads = "downloads/";
		}
		default:
		{
			g_sDownloads = "download/user_custom/cc/";
		}
	}

	CreateConVar(						"spray_exploit_fixer",			PLUGIN_VERSION,		"Spray Exploit Fixer plugin version.", FCVAR_DONTRECORD);
	g_hCvarPunish 	= CreateConVar(		"spray_exploit_fixer_punish",	"3",				"0=Off. 1=PlayerDecal. 2=FileCheck. 3=Both. Which exploits to test for.");
	if( g_iEngine != Engine_TF2 )
	{
		g_hCvarBan = CreateConVar(		"spray_exploit_fixer_ban",		"0",				"0=Off. 1=Ban users who trigger invalid sprays (may still be some false positives).");
		g_hCvarKick = CreateConVar(		"spray_exploit_fixer_kick",		"0",				"0=Off. 1=Kick users who trigger invalid sprays (may still be some false positives).");
		g_hCvarBanTime 	= CreateConVar( "spray_exploit_fixer_bantime",  "5", 				"0=Permanent. Ban time (in minutes).");
	}
	g_hCvarLog = CreateConVar(			"spray_exploit_fixer_log",		"1",				"Logging saved to sourcemod/logs/spray_downloads.log: 0=Off. 1=Log all user uploads. 2=Log invalid sprays only.");
	g_hCvarMsg = CreateConVar(			"spray_exploit_fixer_msg",		"1",				"Print to server console: 0=Off. 1=Missing sprays and invalid sprays. 2=Only invalid sprays.");
	g_hCvarPath = CreateConVar(			"spray_exploit_fixer_path",		g_sDownloads,		"Path to the downloads folder of sprays. Add /cc/ if sprays are stored in individual 2 character folders. Must contain trailing / slash.");
	AutoExecConfig(true,				"spray_exploit_fixer");
	g_hCvarPath.AddChangeHook(ConVarChanged_Cvars);

	g_smChecked = new StringMap();
	g_smReceive = new StringMap();
	g_smWaiting = new StringMap();

	AddTempEntHook("Player Decal", PlayerDecal);

	char sPath[PLATFORM_MAX_PATH];
	strcopy(sPath, sizeof(sPath), g_sDownloads);
	ReplaceString(sPath, sizeof(sPath), "/cc", "");
	StrCat(sPath, sizeof(sPath), PATH_BACKUP);
	CreateDirectory(sPath, 511, true);

	if( !g_bLate )
		MoveSprays();
}

public Action OnPlayerRunCmd(int client, int &buttons, int &impulse, float vel[3], float angles[3])
{
	if( impulse == 0xCA )
	{
		static char cc[6];
		static char sTemp[PLATFORM_MAX_PATH];
		GetPlayerJingleFile(client, sTemp, sizeof(sTemp));

		Format(cc, sizeof(cc), "/%c%c/", sTemp[0], sTemp[1]);
		Format(sTemp, sizeof(sTemp), "%s%s.dat", g_sDownloads, sTemp);
		ReplaceString(sTemp, sizeof(sTemp), "/cc/", cc);

		bool val;
		if( !g_smChecked.GetValue(sTemp, val) || !val )
		{
			impulse = 0;
			return Plugin_Changed;
		}
	}

	return Plugin_Continue;
}

public void OnClientPutInServer(int client)
{
	char sSteamID[64];
	GetClientAuthId(client, AuthId_Steam3, sSteamID, sizeof(sSteamID));
	FormatEx(g_sAuth[client], sizeof(g_sAuth[]), "%s", sSteamID);

	char sSteamIDUnverified[32];
	GetClientAuthId(client, AuthId_Steam3, sSteamIDUnverified, sizeof(sSteamIDUnverified), false);
	FormatEx(g_sAuthUnverified[client], sizeof(g_sAuthUnverified[]), "%s", sSteamIDUnverified);
}

public void OnClientConnected(int client)
{
	g_fSprayed[client] = 0.0;
	g_sPath1[client][0] = 0;
	g_sPath2[client][0] = 0;
}

public void OnClientDisconnect(int client)
{
	if( IsFakeClient(client) ) return;
	
	g_smWaiting.Remove(g_sAuthUnverified[client]);
	g_smChecked.Remove(g_sPath1[client]);
	g_smReceive.Remove(g_sPath1[client]);
	g_smChecked.Remove(g_sPath2[client]);
	g_smReceive.Remove(g_sPath2[client]);

	g_sAuth[client][0] = 0;
	g_sAuth[client][6] = 0;
	g_sAuthUnverified[client][0] = 0;

	/*
	static char sPath[PLATFORM_MAX_PATH];
	static char sOld[PLATFORM_MAX_PATH];
	static char sNew[PLATFORM_MAX_PATH];

	for( int i = 0; i < 2; i++ )
	{
		sPath[0] = 0;

		switch( i )
		{
			case 0:
			{
				if( g_sPath1[client][0] )
					strcopy(sPath, sizeof(sPath), g_sPath1[client]);
				else if( IsClientInGame(client) )
					GetPlayerDecalFile(client, sPath, sizeof(sPath));
			}
			case 1:
			{
				if( g_sPath2[client][0] )
					strcopy(sPath, sizeof(sPath), g_sPath2[client]);
				else if( IsClientInGame(client) )
					GetPlayerJingleFile(client, sPath, sizeof(sPath));
			}
		}

		if( sPath[0] )
		{
			if( i == 0 )
			{
				g_smChecked.Remove(g_sPath1[client]);
				g_smReceive.Remove(g_sPath1[client]);
			}
			else
			{
				g_smChecked.Remove(g_sPath2[client]);
				g_smReceive.Remove(g_sPath2[client]);
			}

			Format(sOld, sizeof(sOld), "%s%s.dat", g_sDownloads, sPath);
			Format(sNew, sizeof(sNew), "%s%s/%s.dat", g_sDownloads, PATH_BACKUP, sPath);

			if( FileExists(sOld) )
			{
				if( FileExists(sNew, true) ) DeleteFile(sNew, true);
				RenameFile(sNew, sOld, true);
			}

			StrCat(sOld, sizeof(sOld), ".ztmp");
			StrCat(sNew, sizeof(sNew), ".ztmp");

			if( FileExists(sOld) )
			{
				if( FileExists(sNew, true) ) DeleteFile(sNew, true);
				RenameFile(sNew, sOld, true);
			}
		}
	}
	*/
}

public void OnMapEnd()
{
	MoveSprays();

	// .Clear() is creating a memory leak
	// g_smReceive.Clear();
	// g_smWaiting.Clear();
	delete g_smReceive;
	delete g_smWaiting;
	g_smReceive = new StringMap();
	g_smWaiting = new StringMap();

	for( int i = 1; i <= MaxClients; i++ )
	{
		g_fSprayed[i] = 0.0;
	}
}

void ConVarChanged_Cvars(Handle convar, const char[] oldValue, const char[] newValue)
{
	g_hCvarPath.GetString(g_sDownloads, sizeof(g_sDownloads));
}

Action CmdSprays(int client, int args)
{
	if( g_bProc )
	{
		ReplyToCommand(client, "[Sprays] Already processing.");
		return Plugin_Handled;
	}

	ReplyToCommand(client, "[Sprays] checking files, please wait...");

	g_iTotal = 0;
	g_bProc = true;
	g_fTime = GetEngineTime();

	ArrayList aList = new ArrayList(ByteCountToCells(PLATFORM_MAX_PATH));

	int pos = StrContains(g_sDownloads, "/");
	if( pos != -1 ) g_sDownloads[pos] = 0;

	RecursiveFiles(aList, false, g_sDownloads);

	if( pos != -1 ) g_sDownloads[pos] = '/';

	int count, counts;
	RecursiveSearchDirs(client, aList, count, counts, false);

	return Plugin_Handled;
}

void RecursiveFiles(ArrayList aList, bool move, const char sDir[PLATFORM_MAX_PATH])
{
	FileType type;
	DirectoryListing hDir;
	File hFile;
	int iRead[4];
	int moving;

	if( DirExists(sDir) )
	{
		hDir = OpenDirectory(sDir, true);

		if( hDir )
		{
			char sPath[PLATFORM_MAX_PATH];

			while( ReadDirEntry(hDir, sPath, sizeof(sPath), type) )
			{
				if( strcmp(sPath, ".") && strcmp(sPath, "..") )
				{
					moving = 0;

					switch( type )
					{
						case FileType_Directory:
						{
							if( !move || strcmp(sPath, PATH_BACKUP) )
							{
								Format(sPath, sizeof(sPath), "%s/%s", sDir, sPath);
								RecursiveFiles(aList, move, sPath);
							}
						}
						case FileType_File:
						{
							int len = strlen(sPath);
							if( len > 4 )
							{
								if( strcmp(sPath[len - 4], ".dat") == 0 )
									moving = 1;
								else if( move && strcmp(sPath[len - 5], ".ztmp") == 0)
									moving = 2;

								if( moving )
								{
									if( moving == 2 )
									{
										Format(sPath, sizeof(sPath), "%s/%s", sDir, sPath);
										ReplaceString(sPath, sizeof(sPath), ".ztmp", "");

										if( FileExists(sPath, true) == false )
										{
											moving = 0;
										}
										else
										{
											StrCat(sPath, sizeof(sPath), ".ztmp");
										}
									}
									else
									{
										Format(sPath, sizeof(sPath), "%s/%s", sDir, sPath);
									}

									if( moving )
									{
										hFile = OpenFile(sPath, "rb", false);
										if( hFile )
										{
											hFile.Read(iRead, sizeof(iRead), 1);
											delete hFile;
											if(
												(iRead[0] == 86 && iRead[1] == 84 && iRead[2] == 70 && iRead[3] == 0) ||
												(moving == 2 && iRead[0] == 76 && iRead[1] == 90 && iRead[2] == 83 && iRead[3] == 83)
											)
											{
												aList.PushString(sPath);
											}
										}
									}
								}
							}
						}
					}
				}
			}

			delete hDir;
		}
	}
}

void MoveSprays()
{
	int count, counts;

	ArrayList aList = new ArrayList(ByteCountToCells(PLATFORM_MAX_PATH));

	strcopy(g_sMoveFiles, sizeof(g_sMoveFiles), g_sDownloads);

	int pos = StrContains(g_sMoveFiles, "/");
	if( pos != -1 ) g_sMoveFiles[pos] = 0;

	RecursiveFiles(aList, true, g_sMoveFiles);
	RecursiveSearchDirs(0, aList, count, counts, true);
}

void RecursiveSearchDirs(int client, ArrayList aList, int &count, int &counts, bool move = false)
{
	static char sNew[PLATFORM_MAX_PATH];
	static char sPath[PLATFORM_MAX_PATH];

	File hFile;
	int iRead[sizeof(g_iVal)];
	int len, pos, i;

	while( aList.Length > 0 )
	{
		aList.GetString(0, sPath, sizeof(sPath));
		aList.Erase(0);

		len = strlen(sPath);
		if( (len > 4 && strcmp(sPath[len - 4], ".dat") == 0) || (move && len > 4 && strcmp(sPath[len - 5], ".ztmp") == 0) )
		{
			if( move )
			{
				pos = FindCharInString(sPath, '/', true);
				if( pos != -1 ) sPath[pos] = 0;

				Format(sNew, sizeof(sNew), "%s/%s/%s", g_sMoveFiles, PATH_BACKUP, sPath[pos + 1]);
				if( FileExists(sNew, true) ) DeleteFile(sNew, true);

				if( pos != -1 ) sPath[pos] = '/';

				RenameFile(sNew, sPath, true);
			}
			else
			{
				counts++;

				hFile = OpenFile(sPath, "rb");
				if( hFile )
				{
					hFile.Read(iRead, sizeof(iRead), 1);
	
					delete hFile;

					i = ValFile(iRead);
					if( i != -1 )
					{
						count++;

						PrintToConsole(client, "Invalid file: %s: %02d (%02X <> %02X)", sPath, i, iRead[i], g_iVal[i]);
					}
				}
			}
		}

		if( g_iTotal++ > MAX_READ )
		{
			g_iTotal = 0;

			DataPack dPack;
			CreateDataTimer(0.1, TimerNext, dPack);
			dPack.WriteCell(client);
			dPack.WriteCell(aList);
			dPack.WriteCell(count);
			dPack.WriteCell(counts);
			dPack.WriteCell(move);
			return;
		}
	}

	if( aList.Length == 0 )
	{
		if( !move )
		{
			g_bProc = false;

			ReplyToCommand(client, "[Sprays] downloads checked. Found %d of %d invalid. Took %0.2f seconds.", count, counts, GetEngineTime() - g_fTime);

			delete aList;
		}
		else
		{
			DeleteEmptyDirs(g_sMoveFiles);

			delete aList;
		}
	}
}

Action TimerNext(Handle timer, DataPack dPack)
{
	ArrayList aList;
	int client, count, counts;
	bool move;

	dPack.Reset();
	client = dPack.ReadCell();
	aList = dPack.ReadCell();
	count = dPack.ReadCell();
	counts = dPack.ReadCell();
	move = dPack.ReadCell();

	RecursiveSearchDirs(client, aList, count, counts, move);

	return Plugin_Continue;
}

void DeleteEmptyDirs(const char sDir[PLATFORM_MAX_PATH])
{
	FileType type;
	DirectoryListing hDir;
	bool del = true;

	if( DirExists(sDir) )
	{
		hDir = OpenDirectory(sDir, true);

		if( hDir )
		{
			char sPath[PLATFORM_MAX_PATH];

			while( ReadDirEntry(hDir, sPath, sizeof(sPath), type) )
			{
				if( strcmp(sPath, ".") && strcmp(sPath, "..") )
				{
					switch( type )
					{
						case FileType_Directory:
						{
							Format(sPath, sizeof(sPath), "%s/%s", sDir, sPath);
							DeleteEmptyDirs(sPath);
							del = false;
						}
						case FileType_File:
						{
							del = false;
						}
					}
				}
			}

			delete hDir;
		}

		if( del )
		{
			RemoveDir(sDir);
		}
	}
}

Action PlayerDecal(const char[] te_name, const int[] Players, int numClients, float delay)
{
	if( g_bDecal ) return Plugin_Continue;

	int client = TE_ReadNum("m_nPlayer");
	if( !client || !IsClientInGame(client) || !CheckCommandAccess(client, "sm_sprays_allowed", 0, true) )
	{
		return Plugin_Handled;
	}

	if( IsFakeClient(client) )
	{
		return Plugin_Continue;
	}

	g_sFilename[0] = 0;
	GetPlayerDecalFile(client, g_sFilename, sizeof(g_sFilename));

	bool val;
	if( g_sFilename[0] )
	{
		char cc[6];
		ReplaceString(g_sFilename, sizeof(g_sFilename), g_sDownloads, "");
		ReplaceString(g_sFilename, sizeof(g_sFilename), ".dat", "");

		Format(cc, sizeof(cc), "/%c%c/", g_sFilename[0], g_sFilename[1]);
		Format(g_sFilename, sizeof(g_sFilename), "%s%s.dat", g_sDownloads, g_sFilename);
		ReplaceString(g_sFilename, sizeof(g_sFilename), "/cc/", cc);

		if( !g_smChecked.GetValue(g_sFilename, val) )
		{
			FileCheck();

			g_smChecked.GetValue(g_sFilename, val);
		}
	}

	if( !val )
	{
		static char auth[64];
		if ( g_sAuth[client][6] == 'I' )
			Format(auth, sizeof(auth), "Unverified: %s", g_sAuthUnverified[client]);
		else
			Format(auth, sizeof(auth), "%s", g_sAuth[client]);

		if( FileExists(g_sFilename) )
		{
			if( GetGameTime() - g_fSprayed[client] > TIMEOUT_LOG )
			{
				g_fSprayed[client] = GetGameTime();
				if( g_hCvarLog.IntValue ) LogCustom("Blocked invalid spray: %s from (%N) [%s]", g_sFilename, client, auth);
				if( g_hCvarMsg.IntValue ) PrintToServer("[Spray Exploit] Blocked invalid spray: %s from (%N) [%s]", g_sFilename, client, auth);
			}

			if( g_hCvarPunish.IntValue == 1 || g_hCvarPunish.IntValue >= 3)
				TestClient(client);
		}
		else
		{
			if( GetGameTime() - g_fSprayed[client] > TIMEOUT_LOG && !g_smWaiting.GetValue(g_sAuthUnverified[client], val) )
			{
				g_fSprayed[client] = GetGameTime();
				g_smWaiting.SetValue(g_sAuthUnverified[client], true);

				if( g_hCvarLog.IntValue ) LogCustom("Blocked unchecked spray - missing file: %s from (%N) [%s]", g_sFilename, client, auth);
				if( g_hCvarMsg.IntValue == 1 ) PrintToServer("[Spray Exploit] Blocked unchecked spray - missing file: %s from (%N) [%s]", g_sFilename, client, auth);
			}
		}

		float vPos[3];
		TE_ReadVector("m_vecOrigin", vPos);
		DataPack hPack = new DataPack();
		hPack.WriteCell(GetClientUserId(client));
		hPack.WriteFloat(vPos[0]);
		hPack.WriteFloat(vPos[1]);
		hPack.WriteFloat(vPos[2]);
		RequestFrame(ReqTempEnt, hPack);
		return Plugin_Handled;
	}

	return Plugin_Continue;
}

void ReqTempEnt(DataPack hPack)
{
	hPack.Reset();

	int client = hPack.ReadCell();
	client = GetClientOfUserId(client);
	if( client )
	{
		float vPos[3];
		vPos[0] = hPack.ReadFloat();
		vPos[1] = hPack.ReadFloat();
		vPos[2] = hPack.ReadFloat();

		g_bDecal = true;
		TE_Start("Player Decal");
		TE_WriteVector("m_vecOrigin", vPos);
		TE_WriteNum("m_nEntity", 0);
		TE_WriteNum("m_nPlayer", client);
		TE_SendToClient(client);
		g_bDecal = false;
	}
	delete hPack;
}

int GetClientFromSpray()
{
	char hex[10];
	for( int i = 1; i <= MaxClients; i++ )
	{
		if( IsClientInGame(i) )
		{
			hex[0] = 0;
			GetPlayerDecalFile(i, hex, sizeof(hex));
			if( hex[0] && StrContains(g_sFilename, hex) != -1 )
				return i;
		}
	}

	return 0;
}

int GetClientFromJingle()
{
	char hex[10];
	for( int i = 1; i <= MaxClients; i++ )
	{
		if( IsClientInGame(i) )
		{
			hex[0] = 0;
			GetPlayerJingleFile(i, hex, sizeof(hex));
			if( hex[0] && StrContains(g_sFilename, hex) != -1 )
				return i;
		}
	}

	return 0;
}

void TestClient(int client)
{
	if( g_iEngine != Engine_TF2 && client )
	{
		if( g_hCvarBan.IntValue )
		{
			int iDuration = g_hCvarBanTime.IntValue;

			if( g_bSourceBans )
				SBPP_BanPlayer(0, client, iDuration, "Invalid spray");
			else if( g_bMaterialAdmin )
				MABanPlayer(0, client, MA_BAN_STEAM, iDuration, "Invalid spray");
			else
				BanClient(client, iDuration, BANFLAG_AUTO, "Invalid spray");

			LogAction(client, -1, "[Spray Exploit] %N %s was banned %d minutes for invalid Spray", client, g_sAuthUnverified[client], iDuration);
			return;
		}
		else if( g_hCvarKick.IntValue )
		{
			KickClient(client, "Invalid spray. Please change it");
			LogAction(client, -1, "[Spray Exploit] %N %s was kicked for invalid Spray.", client, g_sAuthUnverified[client]);
			return;
		}
	}
}

public Action OnFileReceive(int client, const char[] sFile)
{
	strcopy(g_sFilename, sizeof(g_sFilename), sFile);

	client = GetClientFromSpray();
	if( !client ) client = GetClientFromJingle();

	bool log;

	if( client )
	{
		static char sPath[PLATFORM_MAX_PATH];

		GetPlayerDecalFile(client, sPath, sizeof(sPath));
		if( strcmp(sPath, g_sPath1[client]) )
		{
			ReplaceString(sPath, sizeof(sPath), g_sDownloads, "");
			strcopy(g_sPath1[client], sizeof(g_sPath1[]), sPath);
			log = true;
		}

		GetPlayerJingleFile(client, sPath, sizeof(sPath));
		if( strcmp(sPath, g_sPath2[client]) )
		{
			ReplaceString(sPath, sizeof(sPath), g_sDownloads, "");
			strcopy(g_sPath2[client], sizeof(g_sPath2[]), sPath);
			log = true;
		}
	}
	else
	{
		log = true;
	}

	if( log && g_hCvarLog.IntValue == 1 )
	{
		if( client )
		{
			static char auth[64];
			if ( g_sAuth[client][6] == 'I' )
				Format(auth, sizeof(auth), "Unverified: %s", g_sAuthUnverified[client]);
			else
				Format(auth, sizeof(auth), "%s", g_sAuth[client]);

			LogCustom("File received: %s from (%N) [%s]", sFile, client, auth);
		}
		else
		{
			int val;
			if( !g_smReceive.GetValue(sFile, val) )
			{
				g_smReceive.SetValue(sFile, true);
				LogCustom("File received: %s", sFile);
			}
		}
	}

	return Plugin_Continue;
}

public Action OnFileSend(int client, const char[] sFile)
{
	strcopy(g_sFilename, sizeof(g_sFilename), sFile);

	bool val;
	if( g_smChecked.GetValue(sFile, val) )
	{
		if( !val ) return Plugin_Handled;
	} else {
		FileCheck();

		if( g_smChecked.GetValue(sFile, val) )
		{
			if( !val ) return Plugin_Handled;
		}
	}

	return Plugin_Continue;
}

void FileCheck()
{
	if( FileExists(g_sFilename) )
	{
		int len = strlen(g_sFilename);
		if( len > 4 && strcmp(g_sFilename[len - 4], ".dat") == 0 )
		{
			int iRead[sizeof(g_iVal)];
			File hFile = OpenFile(g_sFilename, "rb", false);
			if( hFile )
			{
				hFile.Read(iRead, sizeof(iRead), 1);
				delete hFile;

				int i = ValFile(iRead);

				if( i != -1 )
				{
					int client = GetClientFromSpray();
					if( !client ) client = GetClientFromJingle();
					if( client )
					{
						static char auth[64];
						if ( g_sAuth[client][6] == 'I' )
							Format(auth, sizeof(auth), "Unverified: %s", g_sAuthUnverified[client]);
						else
							Format(auth, sizeof(auth), "%s", g_sAuth[client]);

						if( g_hCvarLog.IntValue ) LogCustom("Invalid spray: %s from (%N) [%s]", g_sFilename, client, auth);
						if( g_hCvarMsg.IntValue ) PrintToServer("[Spray Exploit] Invalid spray: %s: %02d (%02X <> %02X) from (%N) [%s]", g_sFilename, i, iRead[i], g_iVal[i], client, auth);
					} else {
						if( g_hCvarLog.IntValue ) LogCustom("Invalid spray: %s", g_sFilename);
						if( g_hCvarMsg.IntValue ) PrintToServer("[Spray Exploit] Invalid spray: %s: %02d (%02X <> %02X)", g_sFilename, i, iRead[i], g_iVal[i]);
					}

					if( g_hCvarPunish.IntValue >= 2 )
						TestClient(client);

					g_smChecked.SetValue(g_sFilename, false);
					return;
				}

				g_smChecked.SetValue(g_sFilename, true);
			} else {
				if( g_hCvarLog.IntValue ) LogCustom("Missing file: %s", g_sFilename);
				if( g_hCvarMsg.IntValue == 1 ) PrintToServer("[Spray Exploit] Missing file: %s", g_sFilename);
			}
		}
	}
}

int ValFile(int iRead[sizeof(g_iVal)])
{
    //this was suggested by madness to solve new spray exploit december 2024.
    //he was a bit vague, just saying header[21] == 42 && header[24] > 1 has to be blocked
    if (iRead[21] == 42 && iRead[24] > 1)
    {
        LogMessage("blocked spray with madness option.");
        return 21; //should not return -1, should return the iRead value probably.
    }
	if( iRead[0] == 82 && iRead[1] == 73 && iRead[2] == 70 && iRead[3] == 70 && iRead[8] == 87 && iRead[9] == 65 && iRead[10] == 86 && iRead[11] == 69 )
	{
		if( iRead[34] + iRead[35] * 256 == 32 )
			return 34;
		return -1;
	}

	char bytes[10];
	bool read = true;
	int n;

	for( int i = 0; i < sizeof(g_iVal); i++ )
	{
		if( g_iVal[i] == 42 )
		{
			switch( i )
			{
				case 8:	read = iRead[i] <= 5;
				case 16, 18:
				{
					Format(bytes, sizeof(bytes), "%02X%02X", iRead[i+1], iRead[i]);
					n = HexToDec(bytes);
					if( n < 0 || n > 8192 ) read = false;
				}
				case 20:
				{
					Format(bytes, sizeof(bytes), "%02X%02X%02X%02X", iRead[i+3], iRead[i+2], iRead[i+1], iRead[i]);
					n = HexToDec(bytes);
					if( n & (0x8000|0x10000|0x800000) ) read = false;
				}
				/*
				case 25:
				{
					if( iRead[i] > 0 ) read = false;
				}
				// */
			}
		}
		else if( i < 27 )
		{
			read = iRead[i] == g_iVal[i];
		}

		if( !read ) return i;
	}

	return -1;
}

int HexToDec(char[] bytes)
{
	int len = strlen(bytes);
	int base = 1;
	int value = 0;

	for( int i = len - 1; i >= 0; i-- )
	{
		if( bytes[i] >= '0' && bytes[i] <= '9' )
		{
			value += (bytes[i] - 48) * base;
			base = base * 16;
		}

		else if( bytes[i] >= 'A' && bytes[i] <= 'F' )
		{
			value += (bytes[i] - 55) * base;
			base = base * 16;
		}
	}

	return value;
}

void LogCustom(const char[] format, any ...)
{
	static char buffer[512];
	VFormat(buffer, sizeof(buffer), format, 2);

	static char sPath[PLATFORM_MAX_PATH], sTime[32];
	BuildPath(Path_SM, sPath, sizeof(sPath), "logs/spray_downloads.log");
	File file = OpenFile(sPath, "a+");
	FormatTime(sTime, sizeof(sTime), "%d-%b-%Y - %H:%M:%S");
	file.WriteLine("%s: %s", sTime, buffer);
	FlushFile(file);
	delete file;
}