Added generic command hooking mechanism, to replace Reg*Cmd which is intended for command creation (bug 4015, r=pred).
This commit is contained in:
parent
493a756aaa
commit
d8474cfafa
@ -81,6 +81,7 @@ for i in SM.sdkInfo:
|
||||
'smn_datapacks.cpp',
|
||||
'smn_lang.cpp',
|
||||
'sm_srvcmds.cpp',
|
||||
'ConsoleDetours.cpp'
|
||||
]
|
||||
binary.AddSourceFiles('core', files)
|
||||
SM.PostSetupHL2Job(extension, binary, i)
|
||||
|
@ -159,3 +159,4 @@ private:
|
||||
extern ConCmdManager g_ConCmds;
|
||||
|
||||
#endif // _INCLUDE_SOURCEMOD_CONCMDMANAGER_H_
|
||||
|
||||
|
@ -519,45 +519,18 @@ SMCResult CGameConfig::ReadSMC_LeavingSection(const SMCStates *states)
|
||||
}
|
||||
#endif
|
||||
/* First, preprocess the signature */
|
||||
char real_sig[511];
|
||||
unsigned char real_sig[511];
|
||||
size_t real_bytes;
|
||||
size_t length;
|
||||
|
||||
real_bytes = 0;
|
||||
length = strlen(s_TempSig.sig);
|
||||
|
||||
for (size_t i=0; i<length; i++)
|
||||
{
|
||||
if (real_bytes >= sizeof(real_sig))
|
||||
{
|
||||
break;
|
||||
}
|
||||
real_sig[real_bytes++] = s_TempSig.sig[i];
|
||||
if (s_TempSig.sig[i] == '\\'
|
||||
&& s_TempSig.sig[i+1] == 'x')
|
||||
{
|
||||
if (i + 3 >= length)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
/* Get the hex part */
|
||||
char s_byte[3];
|
||||
int r_byte;
|
||||
s_byte[0] = s_TempSig.sig[i+2];
|
||||
s_byte[1] = s_TempSig.sig[i+3];
|
||||
s_byte[2] = '\0';
|
||||
/* Read it as an integer */
|
||||
sscanf(s_byte, "%x", &r_byte);
|
||||
/* Save the value */
|
||||
real_sig[real_bytes-1] = r_byte;
|
||||
/* Adjust index */
|
||||
i += 3;
|
||||
}
|
||||
}
|
||||
real_bytes = UTIL_DecodeHexString(real_sig, sizeof(real_sig), s_TempSig.sig);
|
||||
|
||||
if (real_bytes >= 1)
|
||||
{
|
||||
final_addr = g_MemUtils.FindPattern(addrInBase, real_sig, real_bytes);
|
||||
final_addr = g_MemUtils.FindPattern(addrInBase, (char*)real_sig, real_bytes);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -20,7 +20,7 @@ OBJECTS = AdminCache.cpp CDataPack.cpp ConCmdManager.cpp ConVarManager.cpp CoreC
|
||||
sourcemm_api.cpp sourcemod.cpp MenuStyle_Base.cpp MenuStyle_Valve.cpp MenuManager.cpp \
|
||||
MenuStyle_Radio.cpp ChatTriggers.cpp ADTFactory.cpp MenuVoting.cpp sm_crc32.cpp \
|
||||
frame_hooks.cpp concmd_cleaner.cpp PhraseCollection.cpp NextMap.cpp \
|
||||
NativeOwner.cpp logic_bridge.cpp
|
||||
NativeOwner.cpp logic_bridge.cpp ConsoleDetours.cpp
|
||||
OBJECTS += smn_admin.cpp smn_bitbuffer.cpp smn_console.cpp smn_core.cpp \
|
||||
smn_datapacks.cpp smn_entities.cpp smn_events.cpp smn_fakenatives.cpp \
|
||||
smn_filesystem.cpp smn_gameconfigs.cpp smn_halflife.cpp \
|
||||
|
@ -51,7 +51,7 @@ public: // SMGlobalClass
|
||||
void OnSourceModAllInitialized();
|
||||
public: // IMemoryUtils
|
||||
void *FindPattern(const void *libPtr, const char *pattern, size_t len);
|
||||
private:
|
||||
public:
|
||||
bool GetLibraryInfo(const void *libPtr, DynLibInfo &lib);
|
||||
};
|
||||
|
||||
|
@ -48,6 +48,7 @@
|
||||
#include "GameConfigs.h"
|
||||
#include "ExtensionSys.h"
|
||||
#include <sourcemod_version.h>
|
||||
#include "ConsoleDetours.h"
|
||||
|
||||
PlayerManager g_Players;
|
||||
bool g_OnMapStarted = false;
|
||||
@ -725,6 +726,20 @@ void PlayerManager::OnClientCommand(edict_t *pEntity)
|
||||
}
|
||||
}
|
||||
|
||||
if (g_ConsoleDetours.IsEnabled())
|
||||
{
|
||||
cell_t res2 = g_ConsoleDetours.InternalDispatch(client, args);
|
||||
if (res2 >= Pl_Stop)
|
||||
{
|
||||
g_HL2.PopCommandStack();
|
||||
RETURN_META(MRES_SUPERCEDE);
|
||||
}
|
||||
else if (res2 > res)
|
||||
{
|
||||
res = res2;
|
||||
}
|
||||
}
|
||||
|
||||
cell_t res2 = Pl_Continue;
|
||||
m_clcommand->PushCell(client);
|
||||
m_clcommand->PushCell(argcount);
|
||||
|
@ -39,6 +39,9 @@
|
||||
|
||||
#if SOURCE_ENGINE >= SE_ORANGEBOX
|
||||
SH_DECL_HOOK1_void(ICvar, UnregisterConCommand, SH_NOATTRIB, 0, ConCommandBase *);
|
||||
SH_DECL_HOOK1_void(ICvar, RegisterConCommand, SH_NOATTRIB, 0, ConCommandBase *);
|
||||
#else
|
||||
SH_DECL_HOOK1_void(ICvar, RegisterConCommandBase, SH_NOATTRIB, 0, ConCommandBase *);
|
||||
#endif
|
||||
|
||||
using namespace SourceHook;
|
||||
@ -51,29 +54,55 @@ struct ConCommandInfo
|
||||
};
|
||||
|
||||
List<ConCommandInfo *> tracked_bases;
|
||||
IConCommandLinkListener *IConCommandLinkListener::head = NULL;
|
||||
|
||||
ConCommandBase *FindConCommandBase(const char *name);
|
||||
|
||||
class ConCommandCleaner : public SMGlobalClass
|
||||
{
|
||||
public:
|
||||
#if SOURCE_ENGINE >= SE_ORANGEBOX
|
||||
void OnSourceModAllInitialized()
|
||||
{
|
||||
#if SOURCE_ENGINE >= SE_ORANGEBOX
|
||||
SH_ADD_HOOK_MEMFUNC(ICvar, UnregisterConCommand, icvar, this, &ConCommandCleaner::UnlinkConCommandBase, false);
|
||||
SH_ADD_HOOK_MEMFUNC(ICvar, RegisterConCommand, icvar, this, &ConCommandCleaner::LinkConCommandBase, false);
|
||||
#else
|
||||
SH_ADD_HOOK_MEMFUNC(ICvar, RegisterConCommandBase, icvar, this, &ConCommandCleaner::LinkConCommandBase, false);
|
||||
#endif
|
||||
}
|
||||
|
||||
void OnSourceModShutdown()
|
||||
{
|
||||
#if SOURCE_ENGINE >= SE_ORANGEBOX
|
||||
SH_REMOVE_HOOK_MEMFUNC(ICvar, UnregisterConCommand, icvar, this, &ConCommandCleaner::UnlinkConCommandBase, false);
|
||||
}
|
||||
SH_REMOVE_HOOK_MEMFUNC(ICvar, RegisterConCommand, icvar, this, &ConCommandCleaner::LinkConCommandBase, false);
|
||||
#else
|
||||
SH_REMOVE_HOOK_MEMFUNC(ICvar, RegisterConCommandBase, icvar, this, &ConCommandCleaner::LinkConCommandBase, false);
|
||||
#endif
|
||||
}
|
||||
|
||||
void LinkConCommandBase(ConCommandBase *pBase)
|
||||
{
|
||||
IConCommandLinkListener *listener = IConCommandLinkListener::head;
|
||||
while (listener)
|
||||
{
|
||||
listener->OnLinkConCommand(pBase);
|
||||
listener = listener->next;
|
||||
}
|
||||
}
|
||||
|
||||
void UnlinkConCommandBase(ConCommandBase *pBase)
|
||||
{
|
||||
ConCommandInfo *pInfo;
|
||||
List<ConCommandInfo *>::iterator iter = tracked_bases.begin();
|
||||
|
||||
IConCommandLinkListener *listener = IConCommandLinkListener::head;
|
||||
while (listener)
|
||||
{
|
||||
listener->OnUnlinkConCommand(pBase);
|
||||
listener = listener->next;
|
||||
}
|
||||
|
||||
if (pBase)
|
||||
{
|
||||
while (iter != tracked_bases.end())
|
||||
|
@ -42,4 +42,26 @@ void TrackConCommandBase(ConCommandBase *pBase, IConCommandTracker *me);
|
||||
void UntrackConCommandBase(ConCommandBase *pBase, IConCommandTracker *me);
|
||||
void Global_OnUnlinkConCommandBase(ConCommandBase *pBase);
|
||||
|
||||
class IConCommandLinkListener
|
||||
{
|
||||
friend class ConCommandCleaner;
|
||||
|
||||
static IConCommandLinkListener *head;
|
||||
IConCommandLinkListener *next;
|
||||
public:
|
||||
IConCommandLinkListener()
|
||||
{
|
||||
next = head;
|
||||
head = this;
|
||||
}
|
||||
|
||||
virtual void OnLinkConCommand(ConCommandBase *pBase)
|
||||
{
|
||||
}
|
||||
|
||||
virtual void OnUnlinkConCommand(ConCommandBase *pBase)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
#endif //_INCLUDE_CONCMD_TRACKER_H_
|
||||
|
@ -142,6 +142,7 @@
|
||||
<Tool
|
||||
Name="VCResourceCompilerTool"
|
||||
PreprocessorDefinitions="BINARY_NAME=\"$(TargetFileName)\""
|
||||
AdditionalIncludeDirectories="..\..\public"
|
||||
/>
|
||||
<Tool
|
||||
Name="VCPreLinkEventTool"
|
||||
@ -389,6 +390,7 @@
|
||||
<Tool
|
||||
Name="VCResourceCompilerTool"
|
||||
PreprocessorDefinitions="BINARY_NAME=\"$(TargetFileName)\""
|
||||
AdditionalIncludeDirectories="..\..\public"
|
||||
/>
|
||||
<Tool
|
||||
Name="VCPreLinkEventTool"
|
||||
@ -1130,6 +1132,7 @@
|
||||
<Tool
|
||||
Name="VCResourceCompilerTool"
|
||||
PreprocessorDefinitions="BINARY_NAME=\"$(TargetFileName)\""
|
||||
AdditionalIncludeDirectories="..\..\public"
|
||||
/>
|
||||
<Tool
|
||||
Name="VCPreLinkEventTool"
|
||||
@ -1377,6 +1380,7 @@
|
||||
<Tool
|
||||
Name="VCResourceCompilerTool"
|
||||
PreprocessorDefinitions="BINARY_NAME=\"$(TargetFileName)\""
|
||||
AdditionalIncludeDirectories="..\..\public"
|
||||
/>
|
||||
<Tool
|
||||
Name="VCPreLinkEventTool"
|
||||
@ -1533,6 +1537,10 @@
|
||||
RelativePath="..\ConCmdManager.cpp"
|
||||
>
|
||||
</File>
|
||||
<File
|
||||
RelativePath="..\ConsoleDetours.cpp"
|
||||
>
|
||||
</File>
|
||||
<File
|
||||
RelativePath="..\ConVarManager.cpp"
|
||||
>
|
||||
@ -1831,6 +1839,14 @@
|
||||
RelativePath="..\ConCmdManager.h"
|
||||
>
|
||||
</File>
|
||||
<File
|
||||
RelativePath="..\ConCommandBaseIterator.h"
|
||||
>
|
||||
</File>
|
||||
<File
|
||||
RelativePath="..\ConsoleDetours.h"
|
||||
>
|
||||
</File>
|
||||
<File
|
||||
RelativePath="..\ConVarManager.h"
|
||||
>
|
||||
|
@ -1580,3 +1580,50 @@ char *UTIL_TrimWhitespace(char *str, size_t &len)
|
||||
return str;
|
||||
}
|
||||
|
||||
size_t UTIL_DecodeHexString(unsigned char *buffer, size_t maxlength, const char *hexstr)
|
||||
{
|
||||
size_t written = 0;
|
||||
size_t length = strlen(hexstr);
|
||||
|
||||
for (size_t i = 0; i < length; i++)
|
||||
{
|
||||
if (written >= maxlength)
|
||||
break;
|
||||
buffer[written++] = hexstr[i];
|
||||
if (hexstr[i] == '\\' && hexstr[i + 1] == 'x')
|
||||
{
|
||||
if (i + 3 >= length)
|
||||
continue;
|
||||
/* Get the hex part. */
|
||||
char s_byte[3];
|
||||
int r_byte;
|
||||
s_byte[0] = hexstr[i + 2];
|
||||
s_byte[1] = hexstr[i + 3];
|
||||
s_byte[2] = '\0';
|
||||
/* Read it as an integer */
|
||||
sscanf(s_byte, "%x", &r_byte);
|
||||
/* Save the value */
|
||||
buffer[written - 1] = r_byte;
|
||||
/* Adjust index */
|
||||
i += 3;
|
||||
}
|
||||
}
|
||||
|
||||
return written;
|
||||
}
|
||||
|
||||
char *UTIL_ToLowerCase(const char *str)
|
||||
{
|
||||
size_t len = strlen(str);
|
||||
char *buffer = new char[len + 1];
|
||||
for (size_t i = 0; i < len; i++)
|
||||
{
|
||||
if (str[i] >= 'A' && str[i] <= 'Z')
|
||||
buffer[i] = tolower(str[i]);
|
||||
else
|
||||
buffer[i] = str[i];
|
||||
}
|
||||
buffer[len] = '\0';
|
||||
return buffer;
|
||||
}
|
||||
|
||||
|
@ -60,5 +60,8 @@ char *sm_strdup(const char *str);
|
||||
unsigned int UTIL_ReplaceAll(char *subject, size_t maxlength, const char *search, const char *replace, bool caseSensitive = true);
|
||||
char *UTIL_ReplaceEx(char *subject, size_t maxLen, const char *search, size_t searchLen, const char *replace, size_t replaceLen, bool caseSensitive = true);
|
||||
char *UTIL_TrimWhitespace(char *str, size_t &len);
|
||||
size_t UTIL_DecodeHexString(unsigned char *buffer, size_t maxlength, const char *hexstr);
|
||||
char *UTIL_ToLowerCase(const char *str);
|
||||
|
||||
#endif // _INCLUDE_SOURCEMOD_STRINGUTIL_H_
|
||||
|
||||
|
@ -43,6 +43,8 @@
|
||||
#include <inetchannel.h>
|
||||
#include <bitbuf.h>
|
||||
#include <sm_trie_tpl.h>
|
||||
#include "Logger.h"
|
||||
#include "ConsoleDetours.h"
|
||||
|
||||
#if SOURCE_ENGINE == SE_LEFT4DEAD
|
||||
#define NET_SETCONVAR 6
|
||||
@ -668,7 +670,7 @@ static cell_t sm_RegServerCmd(IPluginContext *pContext, const cell_t *params)
|
||||
|
||||
if (strcasecmp(name, "sm") == 0)
|
||||
{
|
||||
return pContext->ThrowNativeError("Cannot override \"sm\" command");
|
||||
return pContext->ThrowNativeError("Cannot register \"sm\" command");
|
||||
}
|
||||
|
||||
pContext->LocalToString(params[3], &help);
|
||||
@ -696,7 +698,7 @@ static cell_t sm_RegConsoleCmd(IPluginContext *pContext, const cell_t *params)
|
||||
|
||||
if (strcasecmp(name, "sm") == 0)
|
||||
{
|
||||
return pContext->ThrowNativeError("Cannot override \"sm\" command");
|
||||
return pContext->ThrowNativeError("Cannot register \"sm\" command");
|
||||
}
|
||||
|
||||
pContext->LocalToString(params[3], &help);
|
||||
@ -727,7 +729,7 @@ static cell_t sm_RegAdminCmd(IPluginContext *pContext, const cell_t *params)
|
||||
|
||||
if (strcasecmp(name, "sm") == 0)
|
||||
{
|
||||
return pContext->ThrowNativeError("Cannot override \"sm\" command");
|
||||
return pContext->ThrowNativeError("Cannot register \"sm\" command");
|
||||
}
|
||||
|
||||
pContext->LocalToString(params[4], &help);
|
||||
@ -1332,6 +1334,46 @@ static cell_t RemoveServerTag(IPluginContext *pContext, const cell_t *params)
|
||||
return 0;
|
||||
}
|
||||
|
||||
static cell_t AddCommandListener(IPluginContext *pContext, const cell_t *params)
|
||||
{
|
||||
char *name;
|
||||
IPluginFunction *pFunction;
|
||||
|
||||
pContext->LocalToString(params[2], &name);
|
||||
|
||||
if (strcasecmp(name, "sm") == 0)
|
||||
{
|
||||
g_Logger.LogError("Request to register \"sm\" command denied.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
pFunction = pContext->GetFunctionById(params[1]);
|
||||
|
||||
if (!pFunction)
|
||||
return pContext->ThrowNativeError("Invalid function id (%X)", params[1]);
|
||||
|
||||
if (!g_ConsoleDetours.AddListener(pFunction, name[0] == '\0' ? NULL : name))
|
||||
return pContext->ThrowNativeError("This game does not support command listeners");
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static cell_t RemoveCommandListener(IPluginContext *pContext, const cell_t *params)
|
||||
{
|
||||
char *name;
|
||||
IPluginFunction *pFunction;
|
||||
|
||||
pContext->LocalToString(params[2], &name);
|
||||
pFunction = pContext->GetFunctionById(params[1]);
|
||||
if (!pFunction)
|
||||
return pContext->ThrowNativeError("Invalid function id (%X)", params[1]);
|
||||
|
||||
if (!g_ConsoleDetours.RemoveListener(pFunction, name[0] == '\0' ? NULL : name))
|
||||
return pContext->ThrowNativeError("No matching callback was registered");
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
REGISTER_NATIVES(consoleNatives)
|
||||
{
|
||||
{"CreateConVar", sm_CreateConVar},
|
||||
@ -1381,5 +1423,7 @@ REGISTER_NATIVES(consoleNatives)
|
||||
{"SendConVarValue", SendConVarValue},
|
||||
{"AddServerTag", AddServerTag},
|
||||
{"RemoveServerTag", RemoveServerTag},
|
||||
{"AddCommandListener", AddCommandListener},
|
||||
{"RemoveCommandListener", RemoveCommandListener},
|
||||
{NULL, NULL}
|
||||
};
|
||||
|
@ -44,6 +44,41 @@
|
||||
"linux" "4"
|
||||
}
|
||||
}
|
||||
|
||||
"Keys"
|
||||
{
|
||||
/* Windows */
|
||||
"CES_Patch_Windows" "\xFF\x52\x2C\x5B\x5F\x8B\xC6"
|
||||
"CES_Offset_Windows" "530"
|
||||
"CES_Save_Windows" "3"
|
||||
"CES_Reg_Windows" "1"
|
||||
"CGC_Patch_Windows" "\xFF\x50\x2C\x5F\xB0\x01"
|
||||
"CGC_Offset_Windows" "190"
|
||||
"CGC_Save_Windows" "3"
|
||||
"CGC_Reg_Windows" "1"
|
||||
|
||||
/* Linux i486 */
|
||||
"CES_Patch_Linux_486" "\x89\x1C\x24\xFF\x52\x30"
|
||||
"CES_Offset_Linux_486" "901"
|
||||
"CES_Reg_Linux_486" "3"
|
||||
"CGC_Patch_Linux_486" "\xFF\x57\x30\xBA\x01\x00\x00"
|
||||
"CGC_Offset_Linux_486" "391"
|
||||
|
||||
/* Linux i686 */
|
||||
"CES_Patch_Linux_686" "\x89\x1C\x24\xFF\x52\x30"
|
||||
"CES_Offset_Linux_686" "901"
|
||||
"CES_Reg_Linux_686" "3"
|
||||
"CGC_Patch_Linux_686" "\xFF\x57\x30\xBA\x01\x00\x00"
|
||||
"CGC_Offset_Linux_686" "391"
|
||||
|
||||
/* Linux AMD */
|
||||
"CES_Patch_Linux_AMD" "\xFF\x52\x30\x89\xDA\x83\xC4\x10"
|
||||
"CES_Offset_Linux_AMD" "916"
|
||||
"CES_Save_Linux_AMD" "3"
|
||||
"CGC_Patch_Linux_AMD" "\x89\x1C\x24\xFF\x52\x30"
|
||||
"CGC_Offset_Linux_AMD" "380"
|
||||
"CGC_Reg_Linux_AMD" "3"
|
||||
}
|
||||
|
||||
"Signatures"
|
||||
{
|
||||
@ -52,11 +87,27 @@
|
||||
"library" "server"
|
||||
"windows" "\xE8\x2A\x2A\x2A\x2A\xE8\x2A\x2A\x2A\x2A\xB9\x2A\x2A\x2A\x2A\xE8\x2A\x2A\x2A\x2A\xE8"
|
||||
}
|
||||
|
||||
"gEntList"
|
||||
{
|
||||
"library" "server"
|
||||
"linux" "@gEntList"
|
||||
}
|
||||
|
||||
"Cmd_ExecuteString"
|
||||
{
|
||||
"library" "engine"
|
||||
"linux" "@_Z17Cmd_ExecuteStringPKc12cmd_source_t"
|
||||
"windows" "\x8B\x4C\x24\x04\x8B\x44\x24\x08\x51\xA3\x2A\x2A\x2A\x2A\xE8\x2A\x2A\x2A\x2A\x83\xC4\x04\x83\x3D"
|
||||
}
|
||||
|
||||
"CGameClient::ExecuteString"
|
||||
{
|
||||
"library" "engine"
|
||||
"linux" "@_ZN11CGameClient20ExecuteStringCommandEPKc"
|
||||
"windows" "\x56\x8B\x74\x24\x08\x57\x56\x8B\xF9\xE8\x2A\x2A\x2A\x2A\x84\xC0\x0F\x85\xC4\x00\x00\x00\x56\x8D"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -835,3 +835,52 @@ native AddServerTag(const String:tag[]);
|
||||
* @noreturn
|
||||
*/
|
||||
native RemoveServerTag(const String:tag[]);
|
||||
|
||||
/**
|
||||
* Callback for command listeners. This is invoked whenever any command
|
||||
* reaches the server, from the server console itself or a player.
|
||||
|
||||
* Returning Plugin_Handled or Plugin_Stop will prevent the original,
|
||||
* baseline code from running.
|
||||
*
|
||||
* -- TEXT BELOW IS IMPLEMENTATION, AND NOT GUARANTEED --
|
||||
* Even if returning Plugin_Handled or Plugin_Stop, some callbacks will still
|
||||
* trigger. These are:
|
||||
* * C++ command dispatch hooks from Metamod:Source plugins
|
||||
* * Reg*Cmd() hooks that did not create new commands.
|
||||
*
|
||||
* @param client Client, or 0 for server. Client will be connected but
|
||||
* not necessarily in game.
|
||||
* @param command Command name, lower case. To get name as typed, use
|
||||
* GetCmdArg() and specify argument 0.
|
||||
* @param argc Argument count.
|
||||
* @return Action to take (see extended notes above).
|
||||
*/
|
||||
functag public Action:CommandListener(client, const String:command[], argc);
|
||||
|
||||
/**
|
||||
* Adds a callback that will fire when a command is sent to the server.
|
||||
*
|
||||
* Registering commands is designed to create a new command as part of the UI,
|
||||
* whereas this is a lightweight hook on a command string, existing or not.
|
||||
* Using Reg*Cmd to intercept is in poor practice, as it physically creates a
|
||||
* new command and can slow down dispatch in general.
|
||||
*
|
||||
* @param callback Callback.
|
||||
* @param command Command, or if not specified, a global listener.
|
||||
* The command is case insensitive.
|
||||
* @return True if this feature is available on the current game,
|
||||
* false otherwise.
|
||||
*/
|
||||
native bool:AddCommandListener(CommandListener:callback, const String:command[]="");
|
||||
|
||||
/**
|
||||
* Removes a previously added command listener, in reverse order of being added.
|
||||
*
|
||||
* @param callback Callback.
|
||||
* @param command Command, or if not specified, a global listener.
|
||||
* The command is case insensitive.
|
||||
* @error Callback has no active listeners.
|
||||
*/
|
||||
native RemoveCommandListener(CommandListener:callback, const String:command[]="");
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user