From c843825e7175c17fe4231eaef6159bedf1462961 Mon Sep 17 00:00:00 2001 From: David Anderson Date: Wed, 6 Jun 2007 22:16:15 +0000 Subject: [PATCH] - added experimental (so far) support for SourceMod chat triggers - added ReplyToCommand() for replying to triggers nicely --HG-- extra : convert_revision : svn%3A39bc706e-5318-0410-9160-8a85361fbb7c/trunk%40899 --- core/ChatTriggers.cpp | 232 +++++++++++++++++++++++++++++++++ core/ChatTriggers.h | 37 ++++++ core/ConCmdManager.cpp | 24 +++- core/ConCmdManager.h | 1 + core/msvc8/sourcemod_mm.vcproj | 8 ++ core/sm_globals.h | 7 + core/smn_console.cpp | 76 +++++++++++ core/smn_halflife.cpp | 23 ---- core/sourcemm_api.cpp | 1 + core/sourcemod.cpp | 10 ++ core/sourcemod.h | 1 + plugins/include/console.inc | 17 ++- 12 files changed, 409 insertions(+), 28 deletions(-) create mode 100644 core/ChatTriggers.cpp create mode 100644 core/ChatTriggers.h diff --git a/core/ChatTriggers.cpp b/core/ChatTriggers.cpp new file mode 100644 index 00000000..65b5660a --- /dev/null +++ b/core/ChatTriggers.cpp @@ -0,0 +1,232 @@ +#include "ChatTriggers.h" +#include "sm_stringutil.h" +#include "TextParsers.h" +#include "ConCmdManager.h" + +/* :HACKHACK: We can't SH_DECL here because ConCmdManager.cpp does */ +extern bool __SourceHook_FHRemoveConCommandDispatch(void *, bool, class fastdelegate::FastDelegate0); +extern int __SourceHook_FHAddConCommandDispatch(void *, bool, class fastdelegate::FastDelegate0); + +ChatTriggers g_ChatTriggers; + +ChatTriggers::ChatTriggers() : m_pSayCmd(NULL), m_bWillProcessInPost(false), + m_ReplyTo(SM_REPLY_CONSOLE) +{ + m_PubTrigger = sm_strdup("!"); + m_PrivTrigger = sm_strdup("/"); + m_PubTriggerSize = 1; + m_PrivTriggerSize = 1; +} + +void ChatTriggers::OnSourceModGameInitialized() +{ + ConCommandBase *pCmd = icvar->GetCommands(); + const char *name; + while (pCmd) + { + if (pCmd->IsCommand()) + { + name = pCmd->GetName(); + if (strcmp(name, "say") == 0) + { + m_pSayCmd = (ConCommand *)pCmd; + break; + } + } + pCmd = const_cast(pCmd->GetNext()); + } + + if (m_pSayCmd) + { + SH_ADD_HOOK_MEMFUNC(ConCommand, Dispatch, m_pSayCmd, this, &ChatTriggers::OnSayCommand_Pre, false); + SH_ADD_HOOK_MEMFUNC(ConCommand, Dispatch, m_pSayCmd, this, &ChatTriggers::OnSayCommand_Post, true); + } +} + +void ChatTriggers::OnSourceModShutdown() +{ + if (m_pSayCmd) + { + 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); + } +} + +void ChatTriggers::OnSayCommand_Pre() +{ + int client = g_ConCmds.GetCommandClient(); + + /* The server console cannot do this */ + if (client == 0) + { + RETURN_META(MRES_IGNORED); + } + + const char *args = engine->Cmd_Args(); + + /* Handle quoted string sets */ + bool is_quoted = false; + if (args[0] == '"') + { + args++; + is_quoted = true; + } + + bool is_trigger = false; + bool is_silent = false; + + /* Check for either trigger */ + if (m_PubTriggerSize && strncmp(args, m_PubTrigger, m_PubTriggerSize) == 0) + { + is_trigger = true; + } else if (m_PrivTriggerSize && strncmp(args, m_PrivTrigger, m_PrivTriggerSize) == 0) { + is_trigger = true; + is_silent = true; + } + + if (!is_trigger) + { + RETURN_META(MRES_IGNORED); + } + + /* If we're a public command, process later */ + if (!is_silent) + { + /* We have to process this in _post_ instead. Darn. */ + m_bWillProcessInPost = true; + RETURN_META(MRES_IGNORED); + } + + /* Otherwise, process now */ + if (ProcessTrigger(engine->PEntityOfEntIndex(client), &args[m_PrivTriggerSize], is_quoted)) + { + /* If we succeed, block the original say! */ + RETURN_META(MRES_SUPERCEDE); + } + + RETURN_META(MRES_IGNORED); +} + +void ChatTriggers::OnSayCommand_Post() +{ + if (m_bWillProcessInPost) + { + /* Reset this for re-entrancy */ + m_bWillProcessInPost = false; + + /* Get our arguments */ + const char *args = engine->Cmd_Args(); + + /* Handle quotations */ + bool is_quoted = false; + if (args[0] == '"') + { + args++; + is_quoted = true; + } + + int client = g_ConCmds.GetCommandClient(); + + ProcessTrigger(engine->PEntityOfEntIndex(client), &args[m_PubTriggerSize], is_quoted); + } +} + +bool ChatTriggers::ProcessTrigger(edict_t *pEdict, const char *args, bool is_quoted) +{ + /* Eat up whitespace */ + while (*args != '\0' && IsWhitespace(args)) + { + args++; + } + + /* Check if we're still valid */ + if (*args == '\0') + { + return false; + } + + /* Extract a command. This is kind of sloppy. */ + char cmd_buf[64]; + size_t cmd_len = 0; + const char *inptr = args; + while (*inptr != '\0' + && !IsWhitespace(inptr) + && cmd_len < sizeof(cmd_buf) - 1) + { + cmd_buf[cmd_len++] = *inptr++; + } + cmd_buf[cmd_len] = '\0'; + + /* See if we have this registered */ + bool prepended = false; + if (!g_ConCmds.LookForSourceModCommand(cmd_buf)) + { + /* Check if we had an "sm_" prefix */ + if (strncmp(cmd_buf, "sm_", 3) == 0) + { + return false; + } + + /* Now, prepend. Don't worry about the buffers. This will + * work because the sizes are limited from earlier. + */ + char new_buf[80]; + strcpy(new_buf, "sm_"); + strncopy(&new_buf[3], cmd_buf, sizeof(new_buf)-3); + + /* Recheck */ + if (!g_ConCmds.LookForSourceModCommand(new_buf)) + { + return false; + } + + prepended = true; + } + + /* See if we need to do extra string manipulation */ + if (is_quoted || prepended) + { + static char buffer[300]; + size_t len; + + /* Check if we need to prepend sm_ */ + if (prepended) + { + len = UTIL_Format(buffer, sizeof(buffer), "sm_%s", args); + } else { + len = strncopy(buffer, args, sizeof(buffer)); + } + + /* Check if we need to strip a quote */ + if (is_quoted) + { + if (buffer[len-1] == '"') + { + buffer[--len] = '\0'; + } + } + + args = buffer; + } + + /* Finally, execute! */ + unsigned int old = SetReplyTo(SM_REPLY_CHAT); + serverpluginhelpers->ClientCommand(pEdict, args); + SetReplyTo(old); + + return true; +} + +unsigned int ChatTriggers::SetReplyTo(unsigned int reply) +{ + unsigned int old = m_ReplyTo; + + m_ReplyTo = reply; + + return old; +} + +unsigned int ChatTriggers::GetReplyTo() +{ + return m_ReplyTo; +} diff --git a/core/ChatTriggers.h b/core/ChatTriggers.h new file mode 100644 index 00000000..8000b947 --- /dev/null +++ b/core/ChatTriggers.h @@ -0,0 +1,37 @@ +#ifndef _INCLUDE_SOURCEMOD_CHAT_TRIGGERS_H_ +#define _INCLUDE_SOURCEMOD_CHAT_TRIGGERS_H_ + +#include "sm_globals.h" +#include "sourcemm_api.h" + +#define SM_REPLY_CONSOLE 0 +#define SM_REPLY_CHAT 1 + +class ChatTriggers : public SMGlobalClass +{ +public: + ChatTriggers(); +public: //SMGlobalClass + void OnSourceModGameInitialized(); + void OnSourceModShutdown(); +private: //ConCommand + void OnSayCommand_Pre(); + void OnSayCommand_Post(); +public: + unsigned int GetReplyTo(); + unsigned int SetReplyTo(unsigned int reply); +private: + bool ProcessTrigger(edict_t *pEdict, const char *args, bool is_quoted); +private: + ConCommand *m_pSayCmd; + char *m_PubTrigger; + size_t m_PubTriggerSize; + char *m_PrivTrigger; + size_t m_PrivTriggerSize; + bool m_bWillProcessInPost; + unsigned int m_ReplyTo; +}; + +extern ChatTriggers g_ChatTriggers; + +#endif //_INCLUDE_SOURCEMOD_CHAT_TRIGGERS_H_ diff --git a/core/ConCmdManager.cpp b/core/ConCmdManager.cpp index 9737dde1..949bcaea 100644 --- a/core/ConCmdManager.cpp +++ b/core/ConCmdManager.cpp @@ -58,13 +58,17 @@ void ConCmdManager::OnSourceModAllInitialized() SH_ADD_HOOK_MEMFUNC(IServerGameClients, SetCommandClient, serverClients, this, &ConCmdManager::SetCommandClient, false); ConCommandBase *pCmd = icvar->GetCommands(); + const char *name; while (pCmd) { - if (pCmd->IsCommand() - && strcmp(pCmd->GetName(), "exec") == 0) + if (pCmd->IsCommand()) { - m_pExecCmd = (ConCommand *)pCmd; - break; + name = pCmd->GetName(); + if (strcmp(name, "exec") == 0) + { + m_pExecCmd = (ConCommand *)pCmd; + break; + } } pCmd = const_cast(pCmd->GetNext()); } @@ -756,6 +760,18 @@ void ConCmdManager::RemoveConCmd(ConCmdInfo *info) delete info; } +bool ConCmdManager::LookForSourceModCommand(const char *cmd) +{ + ConCmdInfo *pInfo; + + if (!sm_trie_retrieve(m_pCmds, cmd, (void **)&pInfo)) + { + return false; + } + + return pInfo->sourceMod && (pInfo->conhooks.size() > 0); +} + ConCmdInfo *ConCmdManager::AddOrFindCommand(const char *name, const char *description, int flags) { ConCmdInfo *pInfo; diff --git a/core/ConCmdManager.h b/core/ConCmdManager.h index ef581150..d9e8cbf8 100644 --- a/core/ConCmdManager.h +++ b/core/ConCmdManager.h @@ -101,6 +101,7 @@ public: ResultType DispatchClientCommand(int client, ResultType type); void UpdateAdminCmdFlags(const char *cmd, OverrideType type, FlagBits bits); void NotifyExecDone(const char *file); + bool LookForSourceModCommand(const char *cmd); private: void InternalDispatch(); ResultType RunAdminCommand(ConCmdInfo *pInfo, int client, int args); diff --git a/core/msvc8/sourcemod_mm.vcproj b/core/msvc8/sourcemod_mm.vcproj index e63d036d..f5ba6714 100644 --- a/core/msvc8/sourcemod_mm.vcproj +++ b/core/msvc8/sourcemod_mm.vcproj @@ -189,6 +189,10 @@ RelativePath="..\CDataPack.cpp" > + + @@ -311,6 +315,10 @@ RelativePath="..\CellRecipientFilter.h" > + + diff --git a/core/sm_globals.h b/core/sm_globals.h index 8f86e75d..cf3efc55 100644 --- a/core/sm_globals.h +++ b/core/sm_globals.h @@ -121,6 +121,13 @@ public: virtual void OnSourceModVSPReceived(IServerPluginCallbacks *iface) { } + + /** + * @brief Called once all MM:S plugins are loaded. + */ + virtual void OnSourceModGameInitialized() + { + } private: SMGlobalClass *m_pGlobalClassNext; static SMGlobalClass *head; diff --git a/core/smn_console.cpp b/core/smn_console.cpp index f1449cf5..c792f419 100644 --- a/core/smn_console.cpp +++ b/core/smn_console.cpp @@ -21,6 +21,7 @@ #include "PluginSys.h" #include "sm_stringutil.h" #include "PlayerManager.h" +#include "ChatTriggers.h" enum ConVarBounds { @@ -28,6 +29,8 @@ enum ConVarBounds ConVarBound_Lower }; +#define HUD_PRINTTALK 3 + static cell_t sm_CreateConVar(IPluginContext *pContext, const cell_t *params) { char *name, *defaultVal, *helpText; @@ -632,6 +635,77 @@ static cell_t sm_ClientCommand(IPluginContext *pContext, const cell_t *params) return 1; } + +static cell_t FakeClientCommand(IPluginContext *pContext, const cell_t *params) +{ + CPlayer *pPlayer = g_Players.GetPlayerByIndex(params[1]); + + if (!pPlayer) + { + return pContext->ThrowNativeError("Player %d is not a valid player", params[1]); + } + + if (!pPlayer->IsConnected()) + { + return pContext->ThrowNativeError("Player %d is not connected", params[1]); + } + + char buffer[256]; + g_SourceMod.FormatString(buffer, sizeof(buffer), pContext, params, 2); + + unsigned int old = g_ChatTriggers.SetReplyTo(SM_REPLY_CONSOLE); + serverpluginhelpers->ClientCommand(pPlayer->GetEdict(), buffer); + g_ChatTriggers.SetReplyTo(old); + + return 1; +} + +static cell_t ReplyToCommand(IPluginContext *pContext, const cell_t *params) +{ + /* Build the format string */ + char buffer[1024]; + size_t len = g_SourceMod.FormatString(buffer, sizeof(buffer)-2, pContext, params, 2); + + /* If we're printing to the server, shortcut out */ + if (params[1] == 0) + { + /* Print */ + buffer[len++] = '\n'; + buffer[len] = '\0'; + META_CONPRINT(buffer); + return 1; + } + + CPlayer *pPlayer = g_Players.GetPlayerByIndex(params[1]); + + if (!pPlayer) + { + return pContext->ThrowNativeError("Client index %d is invalid", params[1]); + } + + if (!pPlayer->IsConnected()) + { + return pContext->ThrowNativeError("Client %d is not connected", params[1]); + } + + unsigned int replyto = g_ChatTriggers.GetReplyTo(); + if (replyto == SM_REPLY_CONSOLE) + { + buffer[len++] = '\n'; + buffer[len] = '\0'; + engine->ClientPrintf(pPlayer->GetEdict(), buffer); + } else if (replyto == SM_REPLY_CHAT) { + if (len >= 191) + { + len = 191; + } + buffer[len] = '\0'; + g_HL2.TextMsg(params[1], HUD_PRINTTALK, buffer); + } + + return 1; +} + REGISTER_NATIVES(consoleNatives) { {"CreateConVar", sm_CreateConVar}, @@ -665,5 +739,7 @@ REGISTER_NATIVES(consoleNatives) {"InsertServerCommand", sm_InsertServerCommand}, {"ServerExecute", sm_ServerExecute}, {"ClientCommand", sm_ClientCommand}, + {"FakeClientCommand", FakeClientCommand}, + {"ReplyToCommand", ReplyToCommand}, {NULL, NULL} }; diff --git a/core/smn_halflife.cpp b/core/smn_halflife.cpp index a9c1134c..dcadd99b 100644 --- a/core/smn_halflife.cpp +++ b/core/smn_halflife.cpp @@ -233,28 +233,6 @@ static cell_t IsSoundPrecached(IPluginContext *pContext, const cell_t *params) return enginesound->IsSoundPrecached(sample) ? 1 : 0; } -static cell_t FakeClientCommand(IPluginContext *pContext, const cell_t *params) -{ - CPlayer *pPlayer = g_Players.GetPlayerByIndex(params[1]); - - if (!pPlayer) - { - return pContext->ThrowNativeError("Player %d is not a valid player", params[1]); - } - - if (!pPlayer->IsConnected()) - { - return pContext->ThrowNativeError("Player %d is not connected", params[1]); - } - - char buffer[256]; - g_SourceMod.FormatString(buffer, sizeof(buffer), pContext, params, 2); - - serverpluginhelpers->ClientCommand(pPlayer->GetEdict(), buffer); - - return 1; -} - static cell_t smn_CreateDialog(IPluginContext *pContext, const cell_t *params) { KeyValues *pKV; @@ -354,7 +332,6 @@ REGISTER_NATIVES(halflifeNatives) {"IsGenericPrecached", IsGenericPrecached}, {"PrecacheSound", PrecacheSound}, {"IsSoundPrecached", IsSoundPrecached}, - {"FakeClientCommand", FakeClientCommand}, {"CreateDialog", smn_CreateDialog}, {"PrintToChat", PrintToChat}, {"PrintCenterText", PrintCenterText}, diff --git a/core/sourcemm_api.cpp b/core/sourcemm_api.cpp index e7af5615..e1ded5e3 100644 --- a/core/sourcemm_api.cpp +++ b/core/sourcemm_api.cpp @@ -88,6 +88,7 @@ bool SourceMod_Core::Unpause(char *error, size_t maxlen) void SourceMod_Core::AllPluginsLoaded() { + g_SourceMod.AllPluginsLoaded(); } const char *SourceMod_Core::GetAuthor() diff --git a/core/sourcemod.cpp b/core/sourcemod.cpp index 21a194e6..75f05692 100644 --- a/core/sourcemod.cpp +++ b/core/sourcemod.cpp @@ -656,6 +656,16 @@ IVirtualMachine *SourceModBase::GetScriptingVM() return g_pVM; } +void SourceModBase::AllPluginsLoaded() +{ + SMGlobalClass *base = SMGlobalClass::head; + while (base) + { + base->OnSourceModGameInitialized(); + base = base->m_pGlobalClassNext; + } +} + SMGlobalClass *SMGlobalClass::head = NULL; SMGlobalClass::SMGlobalClass() diff --git a/core/sourcemod.h b/core/sourcemod.h index 0a27b3c9..ff4d9bde 100644 --- a/core/sourcemod.h +++ b/core/sourcemod.h @@ -97,6 +97,7 @@ public: // ISourceMod const char *GetGameFolderName() const; ISourcePawnEngine *GetScriptingEngine(); IVirtualMachine *GetScriptingVM(); + void AllPluginsLoaded(); private: /** * @brief Loading plugins diff --git a/plugins/include/console.inc b/plugins/include/console.inc index 18aaefc1..069fa9cd 100644 --- a/plugins/include/console.inc +++ b/plugins/include/console.inc @@ -133,7 +133,7 @@ native PrintToServer(const String:format[], any:...); /** * Sends a message to a client's console. * - * @param client Player index. + * @param client Client index. * @param format Formatting rules. * @param ... Variable number of format parameters. * @noreturn @@ -141,6 +141,21 @@ native PrintToServer(const String:format[], any:...); */ native PrintToConsole(client, const String:format[], any:...); +/** + * Reples to a message in a command. + * + * A client index of 0 will use PrintToServer(). + * If the command was from the console, PrintToConsole() is used. + * If the command was from chat, PrintToChat() is used. + * + * @param client Client index, or 0 for server. + * @param format Formatting rules. + * @param ... Variable number of format parameters. + * @noreturn + * @error If the client is not connected or invalid. + */ +native ReplyToCommand(client, const String:fornmat[], any:...); + /** * Called when a server-only command is invoked. *