Skip to content

Commit

Permalink
add helper to register client console/chat commands
Browse files Browse the repository at this point in the history
registered and called the same as server commands, which can all be routed to a single function. No more hooking into ClientCommand and adding a bunch of if/else for every plugin. Common pitfalls like forgetting to do case insensitive comparisons can be prevented this way.
  • Loading branch information
wootguy committed Nov 14, 2024
1 parent 801d3b7 commit 2e55072
Show file tree
Hide file tree
Showing 8 changed files with 179 additions and 37 deletions.
6 changes: 3 additions & 3 deletions dlls/CommandArgs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,19 +51,19 @@ 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];
}

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++) {
Expand Down
6 changes: 3 additions & 3 deletions dlls/CommandArgs.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::string> args;
Expand Down
30 changes: 28 additions & 2 deletions dlls/hooks/PluginHooks.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma once
#include "weaponinfo.h"
#include "entity_state.h"
#include "CommandArgs.h"

#define HLCOOP_API_VERSION 2

Expand All @@ -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)();
Expand Down Expand Up @@ -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
Expand Down
109 changes: 99 additions & 10 deletions dlls/hooks/PluginManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include "cbase.h"
#include <fstream>
#include "Scheduler.h"
#include "CBasePlayer.h"

PluginManager g_pluginManager;

Expand All @@ -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];
Expand Down Expand Up @@ -429,7 +433,7 @@ void PluginManager::ReloadPlugins() {
UpdatePluginsFromList(true);
}

void PluginManager::ListPlugins(edict_t* plr) {
void PluginManager::ListPlugins(CBasePlayer* plr) {
std::vector<std::string> lines;

bool isAdmin = !plr || AdminLevel(plr) > ADMIN_NO;
Expand Down Expand Up @@ -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;
}

Expand All @@ -584,34 +593,114 @@ 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) {
ALERT(at_error, "Plugin command limit exceeded! Failed to register: %s\n", cmd);
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);
Expand Down
6 changes: 5 additions & 1 deletion dlls/hooks/PluginManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<typename Func, typename... Args>
HOOK_RETURN_DATA CallHooks(Func hookFunction, Args&&... args) {
HOOK_RETURN_DATA totalRet = {0, 0};
Expand Down
21 changes: 12 additions & 9 deletions dlls/hooks/client_commands.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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
Expand Down
32 changes: 25 additions & 7 deletions dlls/util.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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() );
Expand Down Expand Up @@ -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;
Expand Down
6 changes: 4 additions & 2 deletions dlls/util.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
Expand Down Expand Up @@ -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();

Expand Down

0 comments on commit 2e55072

Please sign in to comment.