diff --git a/core/CConVarManager.cpp b/core/CConVarManager.cpp index 185ecab0..5e54b82b 100644 --- a/core/CConVarManager.cpp +++ b/core/CConVarManager.cpp @@ -15,43 +15,86 @@ #include "CLogger.h" #include "PluginSys.h" #include "sm_srvcmds.h" +#include "sm_stringutil.h" CConVarManager g_ConVarManager; -CConVarManager::CConVarManager() +CConVarManager::CConVarManager() : m_ConVarType(0) { - m_ConVarType = 0; - m_CvarCache = sm_trie_create(); + // Create a convar lookup trie + m_ConVarCache = sm_trie_create(); } CConVarManager::~CConVarManager() { - sm_trie_destroy(m_CvarCache); + List::iterator i; + + // Destroy our convar lookup trie + sm_trie_destroy(m_ConVarCache); + + // Destroy all the ConVarInfo structures + for (i = m_ConVars.begin(); i != m_ConVars.end(); i++) + { + delete (*i); + } + + m_ConVars.clear(); } void CConVarManager::OnSourceModAllInitialized() { HandleAccess sec; + // Set up access rights for the 'ConVar' handle type sec.access[HandleAccess_Read] = 0; - sec.access[HandleAccess_Delete] = HANDLE_RESTRICT_IDENTITY|HANDLE_RESTRICT_OWNER; - sec.access[HandleAccess_Clone] = HANDLE_RESTRICT_IDENTITY|HANDLE_RESTRICT_OWNER; + sec.access[HandleAccess_Delete] = HANDLE_RESTRICT_IDENTITY | HANDLE_RESTRICT_OWNER; + sec.access[HandleAccess_Clone] = HANDLE_RESTRICT_IDENTITY | HANDLE_RESTRICT_OWNER; + // Create the 'ConVar' handle type m_ConVarType = g_HandleSys.CreateType("ConVar", this, 0, NULL, &sec, g_pCoreIdent, NULL); - g_RootMenu.AddRootConsoleCommand("cvars", "View convars associated with a plugin", this); + // Add the 'cvars' option to the 'sm' console command + g_RootMenu.AddRootConsoleCommand("cvars", "View convars created by a plugin", this); } void CConVarManager::OnSourceModShutdown() { + IChangeableForward *fwd; + List::iterator i; + + // Iterate list of ConVarInfo structures + for (i = m_ConVars.begin(); i != m_ConVars.end(); i++) + { + fwd = (*i)->changeForward; + + // Free any convar-change forwards that still exist + if (fwd) + { + g_Forwards.ReleaseForward(fwd); + } + } + + // Remove the 'cvars' option from the 'sm' console command g_RootMenu.RemoveRootConsoleCommand("cvars", this); + // Remove the 'ConVar' handle type g_HandleSys.RemoveType(m_ConVarType, g_pCoreIdent); } void CConVarManager::OnHandleDestroy(HandleType_t type, void *object) { - g_SMAPI->UnregisterConCmdBase(g_PLAPI, static_cast(object)); + ConVarInfo *info; + ConCommandBase *cvar = static_cast(object); + + // Find convar in lookup trie + sm_trie_retrieve(m_ConVarCache, cvar->GetName(), reinterpret_cast(&info)); + + // If convar was created by SourceMod plugin... + if (info->sourceMod) + { + // Then unregister it + g_SMAPI->UnregisterConCmdBase(g_PLAPI, cvar); + } } void CConVarManager::OnRootConsoleCommand(const char *command, unsigned int argcount) @@ -60,15 +103,20 @@ void CConVarManager::OnRootConsoleCommand(const char *command, unsigned int argc { int id = 1; + // Get plugin index that was passed int num = atoi(g_RootMenu.GetArgument(2)); + // If invalid plugin index... if (num < 1 || num > (int)g_PluginSys.GetPluginCount()) { g_RootMenu.ConsolePrint("[SM] Plugin index not found."); return; } + // Get plugin object CPlugin *pl = g_PluginSys.GetPluginByOrder(num); + + // Get number of convars created by plugin int convarnum = pl->GetConVarCount(); if (convarnum == 0) @@ -79,6 +127,7 @@ void CConVarManager::OnRootConsoleCommand(const char *command, unsigned int argc g_RootMenu.ConsolePrint("[SM] Displaying convars for \"%s\":", pl->GetPublicInfo()->name); + // Iterate convar list and display each one for (int i = 0; i < convarnum; i++, id++) { ConVar *cvar = pl->GetConVarByIndex(i); @@ -88,13 +137,14 @@ void CConVarManager::OnRootConsoleCommand(const char *command, unsigned int argc return; } + // Display usage of subcommand g_RootMenu.ConsolePrint("[SM] Usage: sm cvars <#>"); } Handle_t CConVarManager::CreateConVar(IPluginContext *pContext, const char *name, const char *defaultVal, const char *helpText, int flags, bool hasMin, float min, bool hasMax, float max) { ConVar *cvar = NULL; - void *temp = NULL; + ConVarInfo *info = NULL; Handle_t hndl = 0; // Find out if the convar exists already @@ -103,33 +153,59 @@ Handle_t CConVarManager::CreateConVar(IPluginContext *pContext, const char *name // If the convar already exists... if (cvar != NULL) { - IPlugin *pl = g_PluginSys.FindPluginByContext(pContext->GetContext()); - - // This isn't a fatal error because we can handle it, but user should be warned anyways - g_Logger.LogError("[SM] Warning: Plugin \"%s\" has attempted to create already existing convar \"%s\"", pl->GetFilename(), name); - // First check if we already have a handle to it - if (sm_trie_retrieve(m_CvarCache, name, &temp)) + if (sm_trie_retrieve(m_ConVarCache, name, reinterpret_cast(&info))) { // If we do, then return that handle - return reinterpret_cast(temp); + return info->handle; } else { - // If we don't, then create a new handle from the convar and return it - return g_HandleSys.CreateHandle(m_ConVarType, cvar, NULL, g_pCoreIdent, NULL); + // If we don't, then create a new handle from the convar + hndl = g_HandleSys.CreateHandle(m_ConVarType, cvar, NULL, g_pCoreIdent, NULL); + + info = new ConVarInfo; + info->handle = hndl; + info->sourceMod = false; + info->changeForward = NULL; + info->origCallback = cvar->GetCallback(); + + m_ConVars.push_back(info); + + return hndl; } } - // Since we didn't find an existing convar, now we can create it + // To prevent creating a convar that has the same name as a console command... ugh + ConCommandBase *pBase = icvar->GetCommands(); + + while (pBase) + { + if (strcmp(pBase->GetName(), name) == 0) + { + return pContext->ThrowNativeError("Convar \"%s\" was not created. A console command with the same name already exists.", name); + } + + pBase = const_cast(pBase->GetNext()); + } + + // Since we didn't find an existing convar (or concmd with the same name), now we can finally create it! cvar = new ConVar(name, defaultVal, flags, helpText, hasMin, min, hasMax, max); // Add new convar to plugin's list g_PluginSys.GetPluginByCtx(pContext->GetContext())->AddConVar(cvar); - // Create a new handle from the convar + // Create a handle from the new convar hndl = g_HandleSys.CreateHandle(m_ConVarType, cvar, NULL, g_pCoreIdent, NULL); - // Insert the handle into our cache - sm_trie_insert(m_CvarCache, name, reinterpret_cast(hndl)); + info = new ConVarInfo; + info->handle = hndl; + info->sourceMod = true; + info->changeForward = NULL; + info->origCallback = NULL; + + m_ConVars.push_back(info); + + // Insert the handle into our lookup trie + sm_trie_insert(m_ConVarCache, name, info); return hndl; } @@ -137,7 +213,8 @@ Handle_t CConVarManager::CreateConVar(IPluginContext *pContext, const char *name Handle_t CConVarManager::FindConVar(const char *name) { ConVar *cvar = NULL; - void *temp = NULL; + ConVarInfo *info = NULL; + Handle_t hndl = 0; // Search for convar cvar = icvar->FindVar(name); @@ -149,12 +226,139 @@ Handle_t CConVarManager::FindConVar(const char *name) } // At this point, convar exists, so find out if we already have a handle for it - if (sm_trie_retrieve(m_CvarCache, name, &temp)) + if (sm_trie_retrieve(m_ConVarCache, name, reinterpret_cast(&info))) { // If we do, then return that handle - return reinterpret_cast(temp); + return info->handle; } - // If we don't, then create a new handle from the convar and return it - return g_HandleSys.CreateHandle(m_ConVarType, cvar, NULL, g_pCoreIdent, NULL); + // If we don't, then create a new handle from the convar + hndl = g_HandleSys.CreateHandle(m_ConVarType, cvar, NULL, g_pCoreIdent, NULL); + + info = new ConVarInfo; + info->handle = hndl; + info->sourceMod = false; + info->changeForward = NULL; + info->origCallback = cvar->GetCallback(); + + m_ConVars.push_back(info); + + // Insert the handle into our cache + sm_trie_insert(m_ConVarCache, name, info); + + return hndl; +} + +void CConVarManager::HookConVarChange(IPluginContext *pContext, ConVar *cvar, funcid_t funcid) +{ + IPluginFunction *func = pContext->GetFunctionById(funcid); + IChangeableForward *fwd = NULL; + char fwdName[64]; + ConVarInfo *info = NULL; + + // This shouldn't happen... + if (func == NULL) + { + pContext->ThrowNativeError("Invalid function: %d", funcid); + return; + } + + // Create a forward name + UTIL_Format(fwdName, sizeof(fwdName), "ConVar.%s", cvar->GetName()); + + // First find out if the forward already exists + g_Forwards.FindForward(fwdName, &fwd); + + // If the forward doesn't exist... + if (fwd == NULL) + { + // This is the forward's parameter type list + ParamType p[] = {Param_Cell, Param_String, Param_String}; + + // Create the forward + fwd = g_Forwards.CreateForwardEx(fwdName, ET_Ignore, 3, p); + + // Find the convar in the lookup trie + if (sm_trie_retrieve(m_ConVarCache, cvar->GetName(), reinterpret_cast(&info))) + { + // Set the convar's forward to the newly created one + info->changeForward = fwd; + + // Set the convar's callback to our static one + cvar->InstallChangeCallback(OnConVarChanged); + } + } + + // Add the function to the forward's list + fwd->AddFunction(func); +} + +void CConVarManager::UnhookConVarChange(IPluginContext *pContext, ConVar *cvar, funcid_t funcid) +{ + IPluginFunction *func = pContext->GetFunctionById(funcid); + IChangeableForward *fwd = NULL; + ConVarInfo *info = NULL; + + // This shouldn't happen... + if (func == NULL) + { + pContext->ThrowNativeError("Invalid function: %d", funcid); + return; + } + + // Find the convar in the lookup trie + if (sm_trie_retrieve(m_ConVarCache, cvar->GetName(), reinterpret_cast(&info))) + { + // Get the forward + fwd = info->changeForward; + + // If the forward doesn't exist, we can't unhook anything + if (fwd == NULL) + { + pContext->ThrowNativeError("Convar \"%s\" has no active hook.", cvar->GetName()); + return; + } + + // Remove the function from the forward's list + if (!fwd->RemoveFunction(func)) + { + pContext->ThrowNativeError("Function %d is not a valid hook callback for convar \"%s\"", funcid, cvar->GetName()); + return; + } + + // If the forward now has 0 functions in it... + if (fwd->GetFunctionCount() == 0) + { + // Free this forward + g_Forwards.ReleaseForward(fwd); + info->changeForward = NULL; + + // Put the back the original convar callback + cvar->InstallChangeCallback(info->origCallback); + } + } +} + +void CConVarManager::OnConVarChanged(ConVar *cvar, const char *oldValue) +{ + Trie *cache = g_ConVarManager.GetConVarCache(); + ConVarInfo *info; + + // Find the convar in the lookup trie + sm_trie_retrieve(cache, cvar->GetName(), reinterpret_cast(&info)); + + FnChangeCallback origCallback = info->origCallback; + IChangeableForward *fwd = info->changeForward; + + // If there was a change callback installed previously, call it + if (origCallback) + { + origCallback(cvar, oldValue); + } + + // Now call forwards in plugins that have hooked this + fwd->PushCell(info->handle); + fwd->PushString(cvar->GetString()); + fwd->PushString(oldValue); + fwd->Execute(NULL); } \ No newline at end of file diff --git a/core/CConVarManager.h b/core/CConVarManager.h index 6ca9bae7..b21184de 100644 --- a/core/CConVarManager.h +++ b/core/CConVarManager.h @@ -17,9 +17,24 @@ #include "sm_globals.h" #include "sourcemm_api.h" #include "HandleSys.h" +#include "ForwardSys.h" #include "sm_trie.h" +#include #include +using namespace SourceHook; + +/** + * Holds SourceMod-specific information about a convar + */ +struct ConVarInfo +{ + Handle_t handle; /**< Handle to convar */ + bool sourceMod; /**< Determines whether or not convar was created by a SourceMod plugin */ + IChangeableForward *changeForward; /**< Forward associated with convar */ + FnChangeCallback origCallback; /**< The original callback function */ +}; + class CConVarManager : public SMGlobalClass, public IHandleTypeDispatch, @@ -36,17 +51,51 @@ public: // IHandleTypeDispatch public: //IRootConsoleCommand void OnRootConsoleCommand(const char *command, unsigned int argcount); public: + /** + * Get the 'ConVar' handle type ID. + */ inline HandleType_t GetHandleType() { return m_ConVarType; } + + /** + * Get the convar lookup trie. + */ + inline Trie *GetConVarCache() + { + return m_ConVarCache; + } public: + /** + * Create a convar and return a handle to it. + */ Handle_t CreateConVar(IPluginContext *pContext, const char *name, const char *defaultVal, const char *helpText, int flags, bool hasMin, float min, bool hasMax, float max); + + /** + * Searches for a convar and returns a handle to it + */ Handle_t FindConVar(const char* name); + + /** + * Add a function to call when the specified convar changes. + */ + void HookConVarChange(IPluginContext *pContext, ConVar *cvar, funcid_t funcid); + + /** + * Remove a function from the forward that will be called when the specified convar changes. + */ + void UnhookConVarChange(IPluginContext *pContext, ConVar *cvar, funcid_t funcid); +private: + /** + * Static callback that Valve's ConVar class executes when the convar's value changes. + */ + static void OnConVarChanged(ConVar *cvar, const char *oldValue); private: HandleType_t m_ConVarType; - Trie *m_CvarCache; + List m_ConVars; + Trie *m_ConVarCache; }; extern CConVarManager g_ConVarManager; diff --git a/core/smn_convar.cpp b/core/smn_convar.cpp index 716fb70e..ef7acf11 100644 --- a/core/smn_convar.cpp +++ b/core/smn_convar.cpp @@ -25,8 +25,7 @@ static cell_t sm_CreateConVar(IPluginContext *pContext, const cell_t *params) // While the engine seems to accept a blank convar name, it causes a crash upon server quit if (name == NULL || strcmp(name, "") == 0) { - pContext->ThrowNativeError("Null or blank convar name is not allowed."); - return BAD_HANDLE; + return pContext->ThrowNativeError("Null or blank convar name is not allowed."); } pContext->LocalToString(params[2], &defaultVal); @@ -55,6 +54,40 @@ static cell_t sm_FindConVar(IPluginContext *pContext, const cell_t *params) return g_ConVarManager.FindConVar(name); } +static cell_t sm_HookConVarChange(IPluginContext *pContext, const cell_t *params) +{ + Handle_t hndl = static_cast(params[1]); + HandleError err; + ConVar *cvar; + + if ((err=g_HandleSys.ReadHandle(hndl, g_ConVarManager.GetHandleType(), NULL, (void **)&cvar)) + != HandleError_None) + { + return pContext->ThrowNativeError("Invalid ConVar Handle %x (error %d)", hndl, err); + } + + g_ConVarManager.HookConVarChange(pContext, cvar, static_cast(params[2])); + + return 1; +} + +static cell_t sm_UnhookConVarChange(IPluginContext *pContext, const cell_t *params) +{ + Handle_t hndl = static_cast(params[1]); + HandleError err; + ConVar *cvar; + + if ((err=g_HandleSys.ReadHandle(hndl, g_ConVarManager.GetHandleType(), NULL, (void **)&cvar)) + != HandleError_None) + { + return pContext->ThrowNativeError("Invalid ConVar Handle %x (error %d)", hndl, err); + } + + g_ConVarManager.UnhookConVarChange(pContext, cvar, static_cast(params[2])); + + return 1; +} + static cell_t sm_GetConVarBool(IPluginContext *pContext, const cell_t *params) { Handle_t hndl = static_cast(params[1]); @@ -255,6 +288,23 @@ static cell_t sm_GetConVarMax(IPluginContext *pContext, const cell_t *params) return hasMax; } +static cell_t sm_GetConVarName(IPluginContext *pContext, const cell_t *params) +{ + Handle_t hndl = static_cast(params[1]); + HandleError err; + ConVar *cvar; + + if ((err=g_HandleSys.ReadHandle(hndl, g_ConVarManager.GetHandleType(), NULL, (void **)&cvar)) + != HandleError_None) + { + return pContext->ThrowNativeError("Invalid ConVar Handle %x (error %d)", hndl, err); + } + + pContext->StringToLocalUTF8(params[2], params[3], cvar->GetName(), NULL); + + return 1; +} + static cell_t sm_ResetConVar(IPluginContext *pContext, const cell_t *params) { Handle_t hndl = static_cast(params[1]); @@ -274,20 +324,23 @@ static cell_t sm_ResetConVar(IPluginContext *pContext, const cell_t *params) REGISTER_NATIVES(convarNatives) { - {"CreateConVar", sm_CreateConVar}, - {"FindConVar", sm_FindConVar}, - {"GetConVarBool", sm_GetConVarBool}, - {"SetConVarBool", sm_SetConVarNum}, - {"GetConVarInt", sm_GetConVarInt}, - {"SetConVarInt", sm_SetConVarNum}, - {"GetConVarFloat", sm_GetConVarFloat}, - {"SetConVarFloat", sm_SetConVarFloat}, - {"GetConVarString", sm_GetConVarString}, - {"SetConVarString", sm_SetConVarString}, - {"GetConVarFlags", sm_GetConVarFlags}, - {"SetConVarFlags", sm_SetConVarFlags}, - {"GetConVarMin", sm_GetConVarMin}, - {"GetConVarMax", sm_GetConVarMax}, - {"ResetConVar", sm_ResetConVar}, - {NULL, NULL} + {"CreateConVar", sm_CreateConVar}, + {"FindConVar", sm_FindConVar}, + {"HookConVarChange", sm_HookConVarChange}, + {"UnhookConVarChange", sm_UnhookConVarChange}, + {"GetConVarBool", sm_GetConVarBool}, + {"SetConVarBool", sm_SetConVarNum}, + {"GetConVarInt", sm_GetConVarInt}, + {"SetConVarInt", sm_SetConVarNum}, + {"GetConVarFloat", sm_GetConVarFloat}, + {"SetConVarFloat", sm_SetConVarFloat}, + {"GetConVarString", sm_GetConVarString}, + {"SetConVarString", sm_SetConVarString}, + {"GetConVarFlags", sm_GetConVarFlags}, + {"SetConVarFlags", sm_SetConVarFlags}, + {"GetConVarName", sm_GetConVarName}, + {"GetConVarMin", sm_GetConVarMin}, + {"GetConVarMax", sm_GetConVarMax}, + {"ResetConVar", sm_ResetConVar}, + {NULL, NULL} }; diff --git a/plugins/include/console.inc b/plugins/include/console.inc index fdf9cf5e..1a18a93b 100644 --- a/plugins/include/console.inc +++ b/plugins/include/console.inc @@ -74,6 +74,36 @@ native Handle:CreateConVar(const String:name[], const String:defaultValue[], con */ native Handle:FindConVar(const String:name[]); +/** + * Called when a console variable's value is changed. + * + * @param convar Handle to the convar that was changed. + * @param oldValue String containing the value of the convar before it was changed. + * @param newValue String containing the new value of the convar. + * @noreturn + */ +functag OnConVarChanged public(Handle:convar, const String:oldValue[], const String:newValue[]); + +/** + * Creates a hook for when a console variable's value is changed. + * + * @param convar Handle to the convar. + * @param callback An OnConVarChanged function pointer. + * @noreturn + * @error Invalid or corrupt Handle. + */ +native HookConVarChange(Handle:convar, OnConVarChanged:callback); + +/** + * Removes a hook for when a console variable's value is changed. + * + * @param convar Handle to the convar. + * @param callback An OnConVarChanged function pointer. + * @return True on success, false otherwise. + * @error Invalid or corrupt Handle. + */ +native bool:UnhookConVarChange(Handle:convar, OnConVarChanged:callback); + /** * Returns the boolean value of a console variable. * @@ -136,11 +166,11 @@ native SetConVarFloat(Handle:convar, Float:value); * * @param convar Handle to the convar. * @param value Buffer to store the value of the convar. - * @param maxlen Maximum length of string buffer. + * @param maxlength Maximum length of string buffer. * @noreturn * @error Invalid or corrupt Handle. */ -native GetConVarString(Handle:convar, String:value[], maxlen); +native GetConVarString(Handle:convar, String:value[], maxlength); /** * Sets the string value of a console variable. @@ -171,6 +201,17 @@ native GetConVarFlags(Handle:convar); */ native SetConVarFlags(Handle:convar, flags); +/** + * Retrieves the name of a console variable. + * + * @param convar Handle to the convar. + * @param value Buffer to store the name of the convar. + * @param maxlength Maximum length of string buffer. + * @noreturn + * @error Invalid or corrupt Handle. + */ +native GetConVarName(Handle:convar, const String:name[], maxlength); + /** * Retrieves the minimum floating point value that a console variable can contain. * diff --git a/public/sourcepawn/sp_vm_api.h b/public/sourcepawn/sp_vm_api.h index eefe6201..99568264 100644 --- a/public/sourcepawn/sp_vm_api.h +++ b/public/sourcepawn/sp_vm_api.h @@ -224,7 +224,7 @@ namespace SourcePawn class IPluginContext { public: - /** Virtual destructr */ + /** Virtual destructor */ virtual ~IPluginContext() { }; public: /** @@ -265,7 +265,7 @@ namespace SourcePawn virtual IPluginDebugInfo *GetDebugInfo() =0; /** - * @brief Allocs memory on the secondary stack of a plugin. + * @brief Allocates memory on the secondary stack of a plugin. * Note that although called a heap, it is in fact a stack. * * @param cells Number of cells to allocate.