diff --git a/spray_exploit_fixer/scripting/spray_exploit_fixer_2024.sp b/spray_exploit_fixer/scripting/spray_exploit_fixer_2024.sp
new file mode 100644
index 00000000..c4b2bed9
--- /dev/null
+++ b/spray_exploit_fixer/scripting/spray_exploit_fixer_2024.sp
@@ -0,0 +1,1118 @@
+/*
+* 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 .
+*/
+
+
+
+#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
+#tryinclude
+#define REQUIRE_PLUGIN
+
+#pragma newdecls required
+
+#include
+#include
+
+
+#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 -1;
+ }
+ 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;
+}