diff --git a/Client/mods/deathmatch/ClientCommands.cpp b/Client/mods/deathmatch/ClientCommands.cpp index 84529b4c9d5..5b6af538c2b 100644 --- a/Client/mods/deathmatch/ClientCommands.cpp +++ b/Client/mods/deathmatch/ClientCommands.cpp @@ -10,6 +10,7 @@ *****************************************************************************/ #include "StdInc.h" +#include "logic/CRegisteredCommands.h" #include #include #include @@ -57,10 +58,42 @@ bool COMMAND_Executed(const char* szCommand, const char* szArguments, bool bHand strClumpedCommandUTF = strClumpedCommandUTF.substr(0, MAX_COMMAND_LENGTH); strClumpedCommand = UTF16ToMbUTF8(strClumpedCommandUTF); - g_pClientGame->GetRegisteredCommands()->ProcessCommand(szCommandBufferPointer, szArguments); - - // Call the onClientConsole event CClientPlayer* localPlayer = g_pClientGame->GetLocalPlayer(); + + // First try to process with registered Lua commands + CommandExecutionResult commandResult = g_pClientGame->GetRegisteredCommands()->ProcessCommand(szCommandBufferPointer, szArguments, false); + + // If command was handled by Lua, don't send to server + if (commandResult.wasExecuted) + { + return true; // Command was handled locally, don't send to server + } + + // If no Lua handler was found, trigger onClientCommand event to allow interception + CLuaArguments arguments; + arguments.PushString(szCommandBufferPointer); + arguments.PushBoolean(false); // executedByFunction + + if (szArguments && *szArguments) + { + std::istringstream stream{szArguments}; + for (std::string arg; stream >> arg;) + { + arguments.PushString(arg.c_str()); + } + } + + if (localPlayer) + { + localPlayer->CallEvent("onClientCommand", arguments, false); + + // If command was handled by onClientCommand event, don't send to server + if (g_pClientGame->GetEvents()->WasEventCancelled()) + { + return true; // Command was intercepted and handled + } + } + if (localPlayer != nullptr) { @@ -108,7 +141,16 @@ bool COMMAND_Executed(const char* szCommand, const char* szArguments, bool bHand // Call our comand-handlers for core-executed commands too, if allowed if (bAllowScriptedBind) - g_pClientGame->GetRegisteredCommands()->ProcessCommand(szCommand, szArguments); + { + CommandExecutionResult coreCommandResult = g_pClientGame->GetRegisteredCommands()->ProcessCommand(szCommand, szArguments, false); + + // If core command failed, don't show unknown message (these are usually keybinds) + if (!coreCommandResult.wasExecuted) + { + // Silently ignore failed keybind commands to prevent spam + return true; + } + } } return false; } diff --git a/Client/mods/deathmatch/logic/CClientGame.cpp b/Client/mods/deathmatch/logic/CClientGame.cpp index 899a68cf0d8..7066d67e969 100644 --- a/Client/mods/deathmatch/logic/CClientGame.cpp +++ b/Client/mods/deathmatch/logic/CClientGame.cpp @@ -2730,6 +2730,7 @@ void CClientGame::AddBuiltInEvents() // Console events m_Events.AddEvent("onClientConsole", "text", NULL, false); m_Events.AddEvent("onClientCoreCommand", "command", NULL, false); + m_Events.AddEvent("onClientCommand", "command, executedByFunction, ...", NULL, false); // Chat events m_Events.AddEvent("onClientChatMessage", "text, r, g, b, messageType", NULL, false); diff --git a/Client/mods/deathmatch/logic/CRegisteredCommands.cpp b/Client/mods/deathmatch/logic/CRegisteredCommands.cpp index 370176b8797..cbe0987a813 100644 --- a/Client/mods/deathmatch/logic/CRegisteredCommands.cpp +++ b/Client/mods/deathmatch/logic/CRegisteredCommands.cpp @@ -150,13 +150,14 @@ bool CRegisteredCommands::CommandExists(const char* szKey, CLuaMain* pLuaMain) return GetCommand(szKey, pLuaMain) != nullptr; } -bool CRegisteredCommands::ProcessCommand(const char* szKey, const char* szArguments) +CommandExecutionResult CRegisteredCommands::ProcessCommand(const char* szKey, const char* szArguments, bool executedByFunction) { assert(szKey); + CommandExecutionResult result; + // Call the handler for every virtual machine that matches the given key int iCompareResult; - bool bHandled = false; m_bIteratingList = true; list::const_iterator iter = m_Commands.begin(); for (; iter != m_Commands.end(); iter++) @@ -171,14 +172,13 @@ bool CRegisteredCommands::ProcessCommand(const char* szKey, const char* szArgume { // Call it CallCommandHandler((*iter)->pLuaMain, (*iter)->iLuaFunction, (*iter)->strKey, szArguments); - bHandled = true; + result.wasExecuted = true; } } m_bIteratingList = false; TakeOutTheTrash(); - // Return whether some handler was called or not - return bHandled; + return result; } CRegisteredCommands::SCommand* CRegisteredCommands::GetCommand(const char* szKey, class CLuaMain* pLuaMain) diff --git a/Client/mods/deathmatch/logic/CRegisteredCommands.h b/Client/mods/deathmatch/logic/CRegisteredCommands.h index 1fe63e25cfb..481a600f3dd 100644 --- a/Client/mods/deathmatch/logic/CRegisteredCommands.h +++ b/Client/mods/deathmatch/logic/CRegisteredCommands.h @@ -24,6 +24,12 @@ enum class MultiCommandHandlerPolicy : std::uint8_t ALLOW = 2 }; +struct CommandExecutionResult +{ + bool wasCancelled = false; + bool wasExecuted = false; +}; + class CRegisteredCommands { struct SCommand @@ -48,7 +54,7 @@ class CRegisteredCommands void GetCommands(lua_State* luaVM); void GetCommands(lua_State* luaVM, CLuaMain* pTargetLuaMain); - bool ProcessCommand(const char* szKey, const char* szArguments); + CommandExecutionResult ProcessCommand(const char* szKey, const char* szArguments, bool executedByFunction = false); private: SCommand* GetCommand(const char* szKey, class CLuaMain* pLuaMain = NULL); diff --git a/Client/mods/deathmatch/logic/lua/CLuaFunctionDefs.Commands.cpp b/Client/mods/deathmatch/logic/lua/CLuaFunctionDefs.Commands.cpp index e72ca469461..0077a532de4 100644 --- a/Client/mods/deathmatch/logic/lua/CLuaFunctionDefs.Commands.cpp +++ b/Client/mods/deathmatch/logic/lua/CLuaFunctionDefs.Commands.cpp @@ -9,6 +9,7 @@ *****************************************************************************/ #include "StdInc.h" +#include "CRegisteredCommands.h" int CLuaFunctionDefs::AddCommandHandler(lua_State* luaVM) { @@ -90,7 +91,8 @@ int CLuaFunctionDefs::ExecuteCommandHandler(lua_State* luaVM) if (pLuaMain) { // Call it - if (m_pRegisteredCommands->ProcessCommand(strKey, strArgs)) + CommandExecutionResult result = m_pRegisteredCommands->ProcessCommand(strKey, strArgs, true); + if (result.wasExecuted && !result.wasCancelled) { lua_pushboolean(luaVM, true); return 1; diff --git a/Server/mods/deathmatch/logic/CConsole.cpp b/Server/mods/deathmatch/logic/CConsole.cpp index 0106fb1f5f1..577abf79a98 100644 --- a/Server/mods/deathmatch/logic/CConsole.cpp +++ b/Server/mods/deathmatch/logic/CConsole.cpp @@ -85,6 +85,7 @@ bool CConsole::HandleInput(const char* szCommand, CClient* pClient, CClient* pEc // Let the script handle it int iClientType = pClient->GetClientType(); + bool wasHandled = false; switch (iClientType) { @@ -92,7 +93,7 @@ bool CConsole::HandleInput(const char* szCommand, CClient* pClient, CClient* pEc { // See if any registered command can process it CPlayer* pPlayer = static_cast(pClient); - m_pRegisteredCommands->ProcessCommand(szKey, szArguments, pClient); + wasHandled = m_pRegisteredCommands->ProcessCommand(szKey, szArguments, pClient); // HACK: if the client gets destroyed before here, dont continue if (m_pPlayerManager->Exists(pPlayer)) @@ -102,23 +103,39 @@ bool CConsole::HandleInput(const char* szCommand, CClient* pClient, CClient* pEc Arguments.PushString(szCommand); pPlayer->CallEvent("onConsole", Arguments); } + + // If command wasn't handled, send "unknown command" message to client + if (!wasHandled && m_pPlayerManager->Exists(pPlayer)) + { + SString strError("Unknown command or cvar: %s", szKey); + pPlayer->SendEcho(strError); + return false; + } break; } case CClient::CLIENT_CONSOLE: { // See if any registered command can process it CConsoleClient* pConsole = static_cast(pClient); - m_pRegisteredCommands->ProcessCommand(szKey, szArguments, pClient); + wasHandled = m_pRegisteredCommands->ProcessCommand(szKey, szArguments, pClient); // Call the console event CLuaArguments Arguments; Arguments.PushString(szCommand); pConsole->CallEvent("onConsole", Arguments); + + // If command wasn't handled, it's unknown + if (!wasHandled) + { + return false; + } break; } default: break; } + + return wasHandled; } // Doesn't exist