diff --git a/dlls/CommandArgs.cpp b/dlls/CommandArgs.cpp index e6ccfed..16f13ce 100644 --- a/dlls/CommandArgs.cpp +++ b/dlls/CommandArgs.cpp @@ -51,7 +51,7 @@ void CommandArgs::loadArgs() { } } -std::string CommandArgs::ArgV(int idx) { +std::string CommandArgs::ArgV(int idx) const { if (idx >= 0 && idx < (int)args.size()) { return args[idx]; } @@ -59,11 +59,11 @@ std::string CommandArgs::ArgV(int idx) { return ""; } -int CommandArgs::ArgC() { +int CommandArgs::ArgC() const { return args.size(); } -std::string CommandArgs::getFullCommand() { +std::string CommandArgs::getFullCommand() const { std::string str = ArgV(0); for (int i = 1; i < (int)args.size(); i++) { diff --git a/dlls/CommandArgs.h b/dlls/CommandArgs.h index e6e69ad..8b989c0 100644 --- a/dlls/CommandArgs.h +++ b/dlls/CommandArgs.h @@ -13,13 +13,13 @@ struct CommandArgs { EXPORT void loadArgs(); // returns empty string if idx is out of bounds - EXPORT std::string ArgV(int idx); + EXPORT std::string ArgV(int idx) const; // return number of args - EXPORT int ArgC(); + EXPORT int ArgC() const; // return entire command string - EXPORT std::string getFullCommand(); + EXPORT std::string getFullCommand() const; private: std::vector args; diff --git a/dlls/hooks/PluginHooks.h b/dlls/hooks/PluginHooks.h index 2e6212f..f16fef9 100644 --- a/dlls/hooks/PluginHooks.h +++ b/dlls/hooks/PluginHooks.h @@ -1,6 +1,7 @@ #pragma once #include "weaponinfo.h" #include "entity_state.h" +#include "CommandArgs.h" #define HLCOOP_API_VERSION 2 @@ -19,6 +20,24 @@ struct HOOK_RETURN_DATA { #define HOOK_HANDLED {HOOKBIT_HANDLED, 0} #define HOOK_HANDLED_OVERRIDE(data) {(HOOKBIT_HANDLED | HOOKBIT_OVERRIDE), (void*)(data)} +#define FL_CMD_SERVER 1 // command can be sent from server console +#define FL_CMD_CLIENT_CONSOLE 2 // command can be sent from client console +#define FL_CMD_CLIENT_CHAT 4 // command can be sent from client chat +#define FL_CMD_ADMIN 8 // command can only be sent by admins +#define FL_CMD_CASE 16 // command is case sensitive + +// client command that can be run from chat or console +#define FL_CMD_CLIENT (FL_CMD_CLIENT_CONSOLE | FL_CMD_CLIENT_CHAT) + +// command that can be run from anywhere (chat, client console, server console) +#define FL_CMD_ANY (FL_CMD_CLIENT_CONSOLE | FL_CMD_CLIENT_CHAT | FL_CMD_SERVER) + +// Plugin command callback. Handles commands from the server console, client console, and chat. +// pPlayer = player who executed the command, or NULL for the server console +// args = parsed command and arguments with a flag to indicate if the command was sent from chat or console +// return true if the player's chat should be hidden (if the command was sent from chat) +typedef bool (*plugin_cmd_callback)(CBasePlayer* pPlayer, const CommandArgs& args); + struct HLCOOP_PLUGIN_HOOKS { // called when the server starts, after worldspawn is precached and before any entities spawn HOOK_RETURN_DATA (*pfnMapInit)(); @@ -148,8 +167,15 @@ EXPORT void RegisterPlugin(void* plugin, HLCOOP_PLUGIN_HOOKS* hooks, const char* EXPORT cvar_t* RegisterPluginCVar(void* plugin, const char* name, const char* strDefaultValue, int intDefaultValue, int flags); // must call this instead of registering commands directly or else the game crashes when the plugin unloads -// and the registered command is used -EXPORT void RegisterPluginCommand(void* plugin, const char* cmd, void (*function)(void)); +// and the registered command is used. +// cmd = text which triggers the command +// callback = function to call when the text is matched +// flags = combination of FL_CMD_* +// cooldown = limits the speed a command can be triggered by players who are not an admins. +// A non-zero cooldown is recommended so that bad actors can't send hundreds of commands +// simultaneously in an attempt to cause lag or crashes. +EXPORT void RegisterPluginCommand(void* plugin, const char* cmd, plugin_cmd_callback callback, + int flags=FL_CMD_SERVER, float cooldown=0.1f); // boilerplate for PluginInit functions // must be inline so that plugins don't reference the game definition of HLCOOP_API_VERSION diff --git a/dlls/hooks/PluginManager.cpp b/dlls/hooks/PluginManager.cpp index 8d63936..1b71b31 100644 --- a/dlls/hooks/PluginManager.cpp +++ b/dlls/hooks/PluginManager.cpp @@ -4,6 +4,7 @@ #include "cbase.h" #include #include "Scheduler.h" +#include "CBasePlayer.h" PluginManager g_pluginManager; @@ -18,8 +19,11 @@ struct ExternalCvar { struct ExternalCommand { int pluginId; + int flags; + float cooldown; + uint64_t lastCall[32]; // last time a player called this command char name[64]; - void (*function)(void); + plugin_cmd_callback callback; }; ExternalCvar g_plugin_cvars[MAX_PLUGIN_CVARS]; @@ -429,7 +433,7 @@ void PluginManager::ReloadPlugins() { UpdatePluginsFromList(true); } -void PluginManager::ListPlugins(edict_t* plr) { +void PluginManager::ListPlugins(CBasePlayer* plr) { std::vector lines; bool isAdmin = !plr || AdminLevel(plr) > ADMIN_NO; @@ -572,8 +576,13 @@ void ExternalPluginCommand() { } if (!ecmd) { - // should never happen - g_engfuncs.pfnServerPrint(UTIL_VarArgs("Unrecognized external plugin command: %s\n", cmd)); + // can happen if command flags change after reloading a plugin + ALERT(at_console, "Unrecognized external plugin command: %s\n", cmd); + return; + } + + if (!(ecmd->flags & FL_CMD_SERVER)) { + ALERT(at_console, "Plugin command '%s' can only be executed by clients.\n", cmd); return; } @@ -584,14 +593,88 @@ void ExternalPluginCommand() { return; } - ecmd->function(); + CommandArgs args = CommandArgs(); + args.loadArgs(); + + ecmd->callback(NULL, args); +} + +bool PluginManager::ClientCommand(CBasePlayer* pPlayer) { + CommandArgs args = CommandArgs(); + args.loadArgs(); + + ExternalCommand* ecmd = NULL; + + std::string cmd = args.ArgV(0); + std::string cmdLower = toLowerCase(cmd); + + for (int i = 0; i < g_plugin_command_count; i++) { + ExternalCommand& pcmd = g_plugin_commands[i]; + + if (!(g_plugin_commands[i].flags & FL_CMD_CLIENT)) { + continue; + } + + if ((pcmd.flags & FL_CMD_CASE) ? !strcmp(pcmd.name, cmd.c_str()) : toLowerCase(pcmd.name) == cmdLower) { + ecmd = &g_plugin_commands[i]; + break; + } + } + + if (!ecmd) { + return false; + } + + if (args.isConsoleCmd && !(ecmd->flags & FL_CMD_CLIENT_CONSOLE)) { + return false; + } + + if (!args.isConsoleCmd && !(ecmd->flags & FL_CMD_CLIENT_CHAT)) { + return false; + } + + bool isAdmin = AdminLevel(pPlayer) != ADMIN_NO; + + if ((ecmd->flags & FL_CMD_ADMIN) && !isAdmin) { + return false; + } + + if (ecmd->cooldown) { + uint64_t now = getEpochMillis(); + int pidx = pPlayer->entindex() - 1; + float timeSinceCall = TimeDifference(ecmd->lastCall[pidx], now); + if (timeSinceCall < ecmd->cooldown) { + float timeLeft = ecmd->cooldown - timeSinceCall; + if (ecmd->cooldown > 1.0f) { + // let the player know how much time they need to wait if the cooldown is long + UTIL_ClientPrint(pPlayer->edict(), print_center, UTIL_VarArgs("Wait %.1fs", timeLeft)); + UTIL_ClientPrint(pPlayer->edict(), print_console, UTIL_VarArgs("Wait %.1fs before using that command again.\n", timeLeft)); + } + return true; + } + ecmd->lastCall[pidx] = now; + } + + Plugin* plugin = g_pluginManager.FindPlugin(ecmd->pluginId); + + if (!plugin) { + g_engfuncs.pfnServerPrint(UTIL_VarArgs("Command from unloaded plugin can't be called: %s\n", cmd)); + return false; + } + + return ecmd->callback(pPlayer, args); } -void RegisterPluginCommand(void* pluginptr, const char* cmd, void (*function)(void)) { +void RegisterPluginCommand(void* pluginptr, const char* cmd, plugin_cmd_callback callback, int flags, float cooldown) { if (!pluginptr) { return; } + if (!flags) { + ALERT(at_error, "Plugin command flags can't be 0: %s\n", cmd); + return; + } + Plugin* plugin = (Plugin*)pluginptr; if (g_plugin_command_count >= MAX_PLUGIN_COMMANDS) { @@ -599,19 +682,25 @@ void RegisterPluginCommand(void* pluginptr, const char* cmd, void (*function)(vo return; } + std::string cmdLower = (flags & FL_CMD_CASE) ? cmd : toLowerCase(cmd); + for (int i = 0; i < g_plugin_command_count; i++) { - if (!strcmp(g_plugin_commands[i].name, cmd)) { + if (!strcmp(g_plugin_commands[i].name, cmdLower.c_str())) { //g_engfuncs.pfnServerPrint(UTIL_VarArgs("Plugin command already registered: %s\n", cmd)); g_plugin_commands[i].pluginId = plugin->id; - g_plugin_commands[i].function = function; + g_plugin_commands[i].callback = callback; + g_plugin_commands[i].flags = flags; + g_plugin_commands[i].cooldown = cooldown; return; } } ExternalCommand& ecmd = g_plugin_commands[g_plugin_command_count]; ecmd.pluginId = plugin->id; - ecmd.function = function; - strcpy_safe(ecmd.name, cmd, sizeof(ecmd.name)); + ecmd.callback = callback; + ecmd.flags = flags; + ecmd.cooldown = cooldown; + strcpy_safe(ecmd.name, cmdLower.c_str(), sizeof(ecmd.name)); g_plugin_command_count++; g_engfuncs.pfnAddServerCommand(ecmd.name, ExternalPluginCommand); diff --git a/dlls/hooks/PluginManager.h b/dlls/hooks/PluginManager.h index 7506e31..654bb32 100644 --- a/dlls/hooks/PluginManager.h +++ b/dlls/hooks/PluginManager.h @@ -61,12 +61,16 @@ class PluginManager { void ReloadPlugins(); // print loaded server and map plugins to console or client - void ListPlugins(edict_t* plr); + void ListPlugins(CBasePlayer* plr); Plugin* FindPlugin(int id); Plugin* FindPlugin(const char* name); + // run a client command registered by a plugin + // returns true if command should be hidden from chat + bool ClientCommand(CBasePlayer* pPlayer); + template HOOK_RETURN_DATA CallHooks(Func hookFunction, Args&&... args) { HOOK_RETURN_DATA totalRet = {0, 0}; diff --git a/dlls/hooks/client_commands.cpp b/dlls/hooks/client_commands.cpp index 087f14a..3ff3fe0 100644 --- a/dlls/hooks/client_commands.cpp +++ b/dlls/hooks/client_commands.cpp @@ -396,14 +396,6 @@ void ClientCommand(edict_t* pEntity) if (CheatCommand(pEntity)) { return; } - else if (FStrEq(pcmd, "say")) - { - Host_Say(pEntity, 0); - } - else if (FStrEq(pcmd, "say_team")) - { - Host_Say(pEntity, 1); - } else if (FStrEq(pcmd, "fullupdate")) { pPlayer->ForceClientDllUpdate(); @@ -523,12 +515,23 @@ void ClientCommand(edict_t* pEntity) } else if (FStrEq(pcmd, "listplugins")) { - g_pluginManager.ListPlugins(pEntity); + g_pluginManager.ListPlugins(pPlayer); } else if (g_pGameRules->ClientCommand(pPlayer, pcmd)) { // MenuSelect returns true only if the command is properly handled, so don't print a warning } + else if (g_pluginManager.ClientCommand(pPlayer)) { + // plugin handled the command + } + else if (FStrEq(pcmd, "say")) + { + Host_Say(pEntity, 0); + } + else if (FStrEq(pcmd, "say_team")) + { + Host_Say(pEntity, 1); + } else { // tell the user they entered an unknown command diff --git a/dlls/util.cpp b/dlls/util.cpp index 40e060c..a0ef4aa 100644 --- a/dlls/util.cpp +++ b/dlls/util.cpp @@ -1444,8 +1444,13 @@ void UTIL_ClientPrintAll( int msg_dest, const char *msg ) } } -void UTIL_ClientPrint( edict_t* client, int msg_dest, const char * msg) +void UTIL_ClientPrint(edict_t* client, int msg_dest, const char * msg) { + if (!client) { + g_engfuncs.pfnServerPrint(msg); + return; + } + if (msg_dest == print_chat) { MESSAGE_BEGIN(MSG_ONE, gmsgSayText, NULL, client); WRITE_BYTE(0); @@ -1457,9 +1462,17 @@ void UTIL_ClientPrint( edict_t* client, int msg_dest, const char * msg) } } +void UTIL_ClientPrint(CBasePlayer* client, int msg_dest, const char* msg) { + UTIL_ClientPrint(client ? client->edict() : NULL, msg_dest, msg); +} + void UTIL_SayText( const char *pText, CBaseEntity *pEntity ) { - if ( !pEntity || !pEntity->IsNetClient() ) + if (!pEntity) { + g_engfuncs.pfnServerPrint(pText); + return; + } + if ( !pEntity->IsNetClient() ) return; MESSAGE_BEGIN( MSG_ONE, gmsgSayText, NULL, pEntity->edict() ); @@ -2982,17 +2995,22 @@ void LoadAdminList(bool forceUpdate) { g_engfuncs.pfnServerPrint(UTIL_VarArgs("Loaded %d admin(s) from file\n", g_admins.size())); } -int AdminLevel(edict_t* plr) { - std::string steamId = (*g_engfuncs.pfnGetPlayerAuthId)(plr); +int AdminLevel(CBasePlayer* plr) { + if (!plr) { + return ADMIN_OWNER; // probably the server console (called by command callback) + } + + std::string steamId = (*g_engfuncs.pfnGetPlayerAuthId)(plr->edict()); if (!IS_DEDICATED_SERVER()) { - if (ENTINDEX(plr) == 1) { + if (plr->entindex() == 1) { return ADMIN_OWNER; // listen server owner is always the first player to join (I hope) } } - if (g_admins.find(steamId) != g_admins.end()) { - return g_admins[steamId]; + auto adminStatus = g_admins.find(steamId); + if (adminStatus != g_admins.end()) { + return adminStatus->second; } return ADMIN_NO; diff --git a/dlls/util.h b/dlls/util.h index c2a023c..48d7929 100644 --- a/dlls/util.h +++ b/dlls/util.h @@ -429,7 +429,8 @@ class CBasePlayer; EXPORT BOOL UTIL_GetNextBestWeapon( CBasePlayer *pPlayer, CBasePlayerItem *pCurrentWeapon ); // prints messages through the HUD -EXPORT void UTIL_ClientPrint(edict_t* client, int msg_dest, const char *msg ); +EXPORT void UTIL_ClientPrint(edict_t* client, int msg_dest, const char *msg ); // TODO: remove this +EXPORT void UTIL_ClientPrint(CBasePlayer* client, int msg_dest, const char *msg ); // prints a message to the HUD say (chat) EXPORT void UTIL_SayText( const char *pText, CBaseEntity *pEntity ); @@ -812,7 +813,8 @@ EXPORT uint64_t getPlayerCommunityId(edict_t* plr); EXPORT void LoadAdminList(bool forceUpdate=false); // call on each map change, so AdminLevel can work -EXPORT int AdminLevel(edict_t* player); +// returns ADMIN_YES for admins, ADMIN_NO for normal players, ADMIN_OWNER for NULL or listen server host +EXPORT int AdminLevel(CBasePlayer* player); EXPORT uint64_t getEpochMillis();