diff --git a/core/ChatTriggers.cpp b/core/ChatTriggers.cpp index e7526b94..c0b00aae 100644 --- a/core/ChatTriggers.cpp +++ b/core/ChatTriggers.cpp @@ -34,6 +34,8 @@ #include "sm_stringutil.h" #include "ConCmdManager.h" #include "PlayerManager.h" +#include "Translator.h" +#include "HalfLife2.h" /* :HACKHACK: We can't SH_DECL here because ConCmdManager.cpp does. * While the OB build only runs on MM:S 1.6.0+ (SH 5+), the older one @@ -55,6 +57,7 @@ extern bool __SourceHook_FHAddConCommandDispatch(void *, bool, class fastdelegat ChatTriggers g_ChatTriggers; bool g_bSupressSilentFails = false; +CPhraseFile *g_pFloodPhrases = NULL; ChatTriggers::ChatTriggers() : m_pSayCmd(NULL), m_bWillProcessInPost(false), m_bTriggerWasSilent(false), m_ReplyTo(SM_REPLY_CONSOLE) @@ -103,6 +106,20 @@ ConfigResult ChatTriggers::OnSourceModConfigChanged(const char *key, return ConfigResult_Ignore; } +void ChatTriggers::OnSourceModAllInitialized() +{ + m_pShouldFloodBlock = g_Forwards.CreateForward("OnClientFloodCheck", ET_Event, 1, NULL, Param_Cell); + m_pDidFloodBlock = g_Forwards.CreateForward("OnClientFloodResult", ET_Event, 2, NULL, Param_Cell, Param_Cell); +} + +void ChatTriggers::OnSourceModAllInitialized_Post() +{ + unsigned int file_id; + + file_id = g_Translator.FindOrAddPhraseFile("antiflood.phrases.txt"); + g_pFloodPhrases = g_Translator.GetFileByIndex(file_id); +} + void ChatTriggers::OnSourceModGameInitialized() { unsigned int total = 2; @@ -155,6 +172,9 @@ void ChatTriggers::OnSourceModShutdown() SH_REMOVE_HOOK_MEMFUNC(ConCommand, Dispatch, m_pSayCmd, this, &ChatTriggers::OnSayCommand_Post, true); SH_REMOVE_HOOK_MEMFUNC(ConCommand, Dispatch, m_pSayCmd, this, &ChatTriggers::OnSayCommand_Pre, false); } + + g_Forwards.ReleaseForward(m_pShouldFloodBlock); + g_Forwards.ReleaseForward(m_pDidFloodBlock); } #if defined ORANGEBOX_BUILD @@ -167,6 +187,7 @@ void ChatTriggers::OnSayCommand_Pre() #endif int client = g_ConCmds.GetCommandClient(); m_bIsChatTrigger = false; + m_bWasFloodedMessage = false; /* The server console cannot do this */ if (client == 0) @@ -181,6 +202,35 @@ void ChatTriggers::OnSayCommand_Pre() RETURN_META(MRES_IGNORED); } + /* Check if we need to block this message from being sent */ + if (ClientIsFlooding(client)) + { + char buffer[128]; + + /* :TODO: log an error? */ + if (g_Translator.CoreTransEx(g_pFloodPhrases, + client, + buffer, + sizeof(buffer), + "Flooding the server", + NULL, + NULL) + != Trans_Okay) + { + UTIL_Format(buffer, sizeof(buffer), "You are flooding the server!"); + } + + /* :TODO: we should probably kick people who spam too much. */ + + char fullbuffer[192]; + UTIL_Format(fullbuffer, sizeof(fullbuffer), "[SM] %s", buffer); + g_HL2.TextMsg(client, HUD_PRINTTALK, fullbuffer); + + m_bWasFloodedMessage = true; + + RETURN_META(MRES_SUPERCEDE); + } + /* Handle quoted string sets */ bool is_quoted = false; if (args[0] == '"') @@ -197,7 +247,9 @@ void ChatTriggers::OnSayCommand_Pre() { is_trigger = true; args = &args[m_PubTriggerSize]; - } else if (m_PrivTriggerSize && strncmp(args, m_PrivTrigger, m_PrivTriggerSize) == 0) { + } + else if (m_PrivTriggerSize && strncmp(args, m_PrivTrigger, m_PrivTriggerSize) == 0) + { is_trigger = true; is_silent = true; args = &args[m_PrivTriggerSize]; @@ -250,6 +302,7 @@ void ChatTriggers::OnSayCommand_Post() #endif { m_bIsChatTrigger = false; + m_bWasFloodedMessage = false; if (m_bWillProcessInPost) { /* Reset this for re-entrancy */ @@ -355,3 +408,35 @@ bool ChatTriggers::IsChatTrigger() { return m_bIsChatTrigger; } + +bool ChatTriggers::ClientIsFlooding(int client) +{ + bool is_flooding = false; + + if (m_pShouldFloodBlock->GetFunctionCount() != 0) + { + cell_t res = 0; + + m_pShouldFloodBlock->PushCell(client); + m_pShouldFloodBlock->Execute(&res); + + if (res != 0) + { + is_flooding = true; + } + } + + if (m_pDidFloodBlock->GetFunctionCount() != 0) + { + m_pDidFloodBlock->PushCell(client); + m_pDidFloodBlock->PushCell(is_flooding ? 1 : 0); + m_pDidFloodBlock->Execute(NULL); + } + + return is_flooding; +} + +bool ChatTriggers::WasFloodedMessage() +{ + return m_bWasFloodedMessage; +} diff --git a/core/ChatTriggers.h b/core/ChatTriggers.h index c4adbb4a..0da7e549 100644 --- a/core/ChatTriggers.h +++ b/core/ChatTriggers.h @@ -36,6 +36,7 @@ #include "sourcemm_api.h" #include #include +#include class ChatTriggers : public SMGlobalClass { @@ -43,6 +44,8 @@ public: ChatTriggers(); ~ChatTriggers(); public: //SMGlobalClass + void OnSourceModAllInitialized(); + void OnSourceModAllInitialized_Post(); void OnSourceModGameInitialized(); void OnSourceModShutdown(); ConfigResult OnSourceModConfigChanged(const char *key, @@ -62,8 +65,10 @@ public: unsigned int GetReplyTo(); unsigned int SetReplyTo(unsigned int reply); bool IsChatTrigger(); + bool WasFloodedMessage(); private: bool PreProcessTrigger(edict_t *pEdict, const char *args, bool is_quoted); + bool ClientIsFlooding(int client); private: ConCommand *m_pSayCmd; ConCommand *m_pSayTeamCmd; @@ -74,8 +79,11 @@ private: bool m_bWillProcessInPost; bool m_bTriggerWasSilent; bool m_bIsChatTrigger; + bool m_bWasFloodedMessage; unsigned int m_ReplyTo; char m_ToExecute[300]; + IForward *m_pShouldFloodBlock; + IForward *m_pDidFloodBlock; }; extern ChatTriggers g_ChatTriggers; diff --git a/core/ConCmdManager.cpp b/core/ConCmdManager.cpp index 8e27b477..9c7c5d92 100644 --- a/core/ConCmdManager.cpp +++ b/core/ConCmdManager.cpp @@ -299,6 +299,16 @@ void ConCmdManager::InternalDispatch(const CCommand &command) return; } + /* This is a hack to prevent say triggers from firing on messages that were + * blocked because of flooding. We won't remove this, but the hack will get + * "nicer" when we expose explicit say hooks. + */ + if (META_RESULT_STATUS == MRES_SUPERCEDE + && g_ChatTriggers.WasFloodedMessage()) + { + return; + } + cell_t result = Pl_Continue; int args = command.ArgC() - 1; @@ -456,7 +466,9 @@ bool ConCmdManager::CheckCommandAccess(int client, const char *cmd, FlagBits cmd if (rule == Command_Allow) { return true; - } else if (rule == Command_Deny) { + } + else if (rule == Command_Deny) + { return false; } } @@ -486,18 +498,20 @@ bool ConCmdManager::CheckAccess(int client, const char *cmd, AdminCmdInfo *pAdmi if (g_Translator.CoreTrans(client, buffer, sizeof(buffer), "No Access", NULL, NULL) != Trans_Okay) { - snprintf(buffer, sizeof(buffer), "You do not have access to this command"); + UTIL_Format(buffer, sizeof(buffer), "You do not have access to this command"); } unsigned int replyto = g_ChatTriggers.GetReplyTo(); if (replyto == SM_REPLY_CONSOLE) { char fullbuffer[192]; - snprintf(fullbuffer, sizeof(fullbuffer), "[SM] %s.\n", buffer); + UTIL_Format(fullbuffer, sizeof(fullbuffer), "[SM] %s.\n", buffer); engine->ClientPrintf(pEdict, fullbuffer); - } else if (replyto == SM_REPLY_CHAT) { + } + else if (replyto == SM_REPLY_CHAT) + { char fullbuffer[192]; - snprintf(fullbuffer, sizeof(fullbuffer), "[SM] %s.", buffer); + UTIL_Format(fullbuffer, sizeof(fullbuffer), "[SM] %s.", buffer); g_HL2.TextMsg(client, HUD_PRINTTALK, fullbuffer); } diff --git a/core/Translator.cpp b/core/Translator.cpp index 938ad8af..004d8903 100644 --- a/core/Translator.cpp +++ b/core/Translator.cpp @@ -949,25 +949,19 @@ size_t Translator::Translate(char *buffer, size_t maxlength, void **params, cons return gnprintf(buffer, maxlength, pTrans->szPhrase, new_params); } -TransError Translator::CoreTrans(int client, - char *buffer, - size_t maxlength, - const char *phrase, - void **params, - size_t *outlen) +TransError Translator::CoreTransEx(CPhraseFile *pFile, + int client, + char *buffer, + size_t maxlength, + const char *phrase, + void **params, + size_t *outlen) { - /* :TODO: do language stuff here */ - - if (!g_pCorePhrases) - { - return Trans_BadPhraseFile; - } - Translation trans; TransError err; - + /* Using server lang temporarily until client lang stuff is implemented */ - if ((err=g_pCorePhrases->GetTranslation(phrase, m_ServerLang, &trans)) != Trans_Okay) + if ((err = pFile->GetTranslation(phrase, m_ServerLang, &trans)) != Trans_Okay) { return err; } @@ -982,6 +976,21 @@ TransError Translator::CoreTrans(int client, return Trans_Okay; } +TransError Translator::CoreTrans(int client, + char *buffer, + size_t maxlength, + const char *phrase, + void **params, + size_t *outlen) +{ + if (!g_pCorePhrases) + { + return Trans_BadPhraseFile; + } + + return CoreTransEx(g_pCorePhrases, client, buffer, maxlength, phrase, params, outlen); +} + unsigned int Translator::GetServerLanguage() { return m_ServerLang; diff --git a/core/Translator.h b/core/Translator.h index 6ae3b13f..cd3f835d 100644 --- a/core/Translator.h +++ b/core/Translator.h @@ -140,6 +140,13 @@ public: bool GetLanguageByName(const char *name, unsigned int *index); size_t Translate(char *buffer, size_t maxlength, void **params, const Translation *pTrans); CPhraseFile *GetFileByIndex(unsigned int index); + TransError CoreTransEx(CPhraseFile *pFile, + int client, + char *buffer, + size_t maxlength, + const char *phrase, + void **params, + size_t *outlen=NULL); TransError CoreTrans(int client, char *buffer, size_t maxlength, diff --git a/plugins/antiflood.sp b/plugins/antiflood.sp index b74d71c4..c965745a 100644 --- a/plugins/antiflood.sp +++ b/plugins/antiflood.sp @@ -51,10 +51,6 @@ new Handle:sm_flood_time; /* Handle to sm_flood_time convar */ public OnPluginStart() { - LoadTranslations("antiflood.phrases"); - - RegConsoleCmd("say", CheckChatFlood); - RegConsoleCmd("say_team", CheckChatFlood); sm_flood_time = CreateConVar("sm_flood_time", "0.75", "Amount of time allowed between chat messages"); } @@ -64,41 +60,63 @@ public OnClientPutInServer(client) g_FloodTokens[client] = 0; } -public Action:CheckChatFlood(client, args) +new Float:max_chat; + +public bool:OnClientFloodCheck(client) { - /* Chat from server console shouldn't be checked for flooding */ - if (client == 0) + max_chat = GetConVarFloat(sm_flood_time); + + if (max_chat <= 0.0 + || (GetUserFlagBits(client) & ADMFLAG_ROOT) == ADMFLAG_ROOT) { - return Plugin_Continue; + return false; } - new Float:maxChat = GetConVarFloat(sm_flood_time); + PrintToServer("OCFC: %f %f %d", g_LastTime[client], GetGameTime(), g_FloodTokens[client]); - if (maxChat > 0.0) + if (g_LastTime[client] > GetGameTime()) { - new Float:curTime = GetGameTime(); - - if (g_LastTime[client] > curTime) + /* If player has 3 or more flood tokens, block their message */ + if (g_FloodTokens[client] >= 3) { - /* If player has 3 or more flood tokens, block their message */ - if (g_FloodTokens[client] >= 3) - { - PrintToChat(client, "[SM] %t", "Flooding the server"); - g_LastTime[client] = curTime + maxChat + 3.0; - - return Plugin_Stop; - } - - /* Add one flood token when player goes over chat time limit */ - g_FloodTokens[client]++; - } else if (g_FloodTokens[client]) { - /* Remove one flood token when player chats within time limit (slow decay) */ - g_FloodTokens[client]--; + return true; } - - /* Store last time of chat usage */ - g_LastTime[client] = curTime + maxChat; } - return Plugin_Continue; + return false; +} + +public OnClientFloodResult(client, bool:blocked) +{ + if (max_chat <= 0.0 + || (GetUserFlagBits(client) & ADMFLAG_ROOT) == ADMFLAG_ROOT) + { + return; + } + + new Float:curTime = GetGameTime(); + new Float:newTime = curTime + max_chat; + + PrintToServer("OCFR: %f, %f", g_LastTime[client], GetGameTime()); + + if (g_LastTime[client] > curTime) + { + /* If the last message was blocked, update their time limit */ + if (blocked) + { + newTime += 3.0; + } + /* Add one flood token when player goes over chat time limit */ + else if (g_FloodTokens[client] < 3) + { + g_FloodTokens[client]++; + } + } + else if (g_FloodTokens[client] > 0) + { + /* Remove one flood token when player chats within time limit (slow decay) */ + g_FloodTokens[client]--; + } + + g_LastTime[client] = newTime; } diff --git a/plugins/include/sourcemod.inc b/plugins/include/sourcemod.inc index dea00ddb..9fdd0985 100644 --- a/plugins/include/sourcemod.inc +++ b/plugins/include/sourcemod.inc @@ -505,6 +505,32 @@ native Handle:ReadMapList(Handle:array=INVALID_HANDLE, */ native SetMapListCompatBind(const String:name[], const String:file[]); +/** + * Called when a client has sent chat text. This must return either true or + * false to indicate that a client is or is not spamming the server. + * + * The return value is a hint only. Core or another plugin may decide + * otherwise. + * + * @param client Client index. The server (0) will never be passed. + * @return True if client is spamming the server, false otherwise. + */ +forward bool:OnClientFloodCheck(client); + +/** + * Called after a client's flood check has been computed. This can be used + * by antiflood algorithms to decay/increase flooding weights. + * + * Since the result from "OnClientFloodCheck" isn't guaranteed to be the + * final result, it is generally a good idea to use this to play with other + * algorithms nicely. + * + * @param client Client index. The server (0) will never be passed. + * @param blocked True if client flooded last "say", false otherwise. + * @noreturn + */ +forward OnClientFloodResult(client, bool:blocked); + #include #include #include