- 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
This commit is contained in:
David Anderson 2007-06-06 22:16:15 +00:00
parent bd15f93d7d
commit c843825e71
12 changed files with 409 additions and 28 deletions

232
core/ChatTriggers.cpp Normal file
View File

@ -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<void>);
extern int __SourceHook_FHAddConCommandDispatch(void *, bool, class fastdelegate::FastDelegate0<void>);
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<ConCommandBase *>(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;
}

37
core/ChatTriggers.h Normal file
View File

@ -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_

View File

@ -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<ConCommandBase *>(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;

View File

@ -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);

View File

@ -189,6 +189,10 @@
RelativePath="..\CDataPack.cpp"
>
</File>
<File
RelativePath="..\ChatTriggers.cpp"
>
</File>
<File
RelativePath="..\ConCmdManager.cpp"
>
@ -311,6 +315,10 @@
RelativePath="..\CellRecipientFilter.h"
>
</File>
<File
RelativePath="..\ChatTriggers.h"
>
</File>
<File
RelativePath="..\ConCmdManager.h"
>

View File

@ -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;

View File

@ -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}
};

View File

@ -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},

View File

@ -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()

View File

@ -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()

View File

@ -97,6 +97,7 @@ public: // ISourceMod
const char *GetGameFolderName() const;
ISourcePawnEngine *GetScriptingEngine();
IVirtualMachine *GetScriptingVM();
void AllPluginsLoaded();
private:
/**
* @brief Loading plugins

View File

@ -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.
*