diff --git a/amxmodx/amxmodx.cpp b/amxmodx/amxmodx.cpp index 752a30ff..5956214f 100755 --- a/amxmodx/amxmodx.cpp +++ b/amxmodx/amxmodx.cpp @@ -112,6 +112,36 @@ static cell AMX_NATIVE_CALL emit_sound(AMX *amx, cell *params) /* 7 param */ return 1; } +static cell AMX_NATIVE_CALL enable_cvar_hook(AMX *amx, cell *params) /* 7 param */ +{ + Forward* forward = reinterpret_cast(params[1]); + + if (!forward) + { + LogError(amx, AMX_ERR_NATIVE, "Invalid cvar hook handle: %p", forward); + return 0; + } + + forward->state = Forward::FSTATE_OK; + + return 1; +} + +static cell AMX_NATIVE_CALL disable_cvar_hook(AMX *amx, cell *params) /* 7 param */ +{ + Forward* forward = reinterpret_cast(params[1]); + + if (!forward) + { + LogError(amx, AMX_ERR_NATIVE, "Invalid cvar hook handle: %p", forward); + return 0; + } + + forward->state = Forward::FSTATE_STOP; + + return 1; +} + static cell AMX_NATIVE_CALL server_print(AMX *amx, cell *params) /* 1 param */ { int len; @@ -4831,6 +4861,29 @@ static cell AMX_NATIVE_CALL has_map_ent_class(AMX *amx, cell *params) return len && !FNullEnt(FIND_ENTITY_BY_STRING(NULL, "classname", name)); }; +// hook_cvar_change(cvarHandle, const callback[]) +static cell AMX_NATIVE_CALL hook_cvar_change(AMX *amx, cell *params) +{ + cvar_t* var = reinterpret_cast(params[1]); + + if (!var) + { + LogError(amx, AMX_ERR_NATIVE, "Invalid cvar handle: %p", var); + return 0; + } + + const char* callback; + Forward* forward = g_CvarManager.HookCvarChange(var, amx, params[2], &callback); + + if (!forward) + { + LogError(amx, AMX_ERR_NATIVE, "Invalid callback function: %s", callback); + return 0; + } + + return reinterpret_cast(forward); +} + static cell AMX_NATIVE_CALL is_rukia_a_hag(AMX *amx, cell *params) { return 1; @@ -4867,6 +4920,8 @@ AMX_NATIVE_INFO amxmodx_Natives[] = {"console_print", console_print}, {"cvar_exists", cvar_exists}, {"emit_sound", emit_sound}, + {"enable_cvar_hook", enable_cvar_hook}, + {"disable_cvar_hook", disable_cvar_hook}, {"engclient_cmd", engclient_cmd}, {"engclient_print", engclient_print}, {"find_player", find_player}, @@ -4940,6 +4995,7 @@ AMX_NATIVE_INFO amxmodx_Natives[] = {"get_xvar_id", get_xvar_id}, {"get_xvar_num", get_xvar_num}, {"has_map_ent_class", has_map_ent_class}, + {"hook_cvar_change", hook_cvar_change}, {"int3", int3}, {"is_amd64_server", is_amd64_server}, {"is_dedicated_server", is_dedicated_server}, diff --git a/amxmodx/cvars.cpp b/amxmodx/cvars.cpp index 3d7ea8c2..83b67c06 100644 --- a/amxmodx/cvars.cpp +++ b/amxmodx/cvars.cpp @@ -13,19 +13,48 @@ CvarManager g_CvarManager; -DETOUR_DECL_STATIC2(Cvar_DirectSet, void, struct cvar_s*, var, const char*, value) +/** + * Returns true to call original function, otherwise false to block it. + */ +bool Cvar_DirectSet_Custom(cvar_t* var, const char* value) { - // Sanity checks against bogus pointers. - if (var && value) + CvarInfo* info = nullptr; + + if (!var || !value // Sanity checks against bogus pointers. + || strcmp(var->string, value) == 0 // Make sure old and new values are different to not trigger callbacks. + || !g_CvarManager.CacheLookup(var->name, &info) // No data in cache, nothing to call. + || info->hooks.empty()) // No hooked cvars, nothing to call. { - // Make sure old and new values are different to not trigger callbacks. - if (strcmp(var->string, value) != 0) + return true; + } + + int lastResult = 0; + int result; + + for (size_t i = 0; i < info->hooks.length(); ++i) + { + CvarPlugin* p = info->hooks[i]; + + if (p->forward->state == Forward::FSTATE_OK) // Our callback can be enable/disabled by natives. { - + result = executeForwards(p->forward->id, reinterpret_cast(var), var->string, value); + + if (result >= lastResult) + { + lastResult = result; + } } } - DETOUR_STATIC_CALL(Cvar_DirectSet)(var, value); + return !!!lastResult; +} + +DETOUR_DECL_STATIC2(Cvar_DirectSet, void, struct cvar_s*, var, const char*, value) +{ + if (Cvar_DirectSet_Custom(var, value)) + { + DETOUR_STATIC_CALL(Cvar_DirectSet)(var, value); + } } CvarManager::CvarManager() : m_AmxmodxCvars(0), m_HookDetour(nullptr) @@ -34,19 +63,7 @@ CvarManager::CvarManager() : m_AmxmodxCvars(0), m_HookDetour(nullptr) CvarManager::~CvarManager() { - CvarsList::iterator iter = m_Cvars.begin(); - - while (iter != m_Cvars.end()) - { - CvarInfo* info = (*iter); - - iter = m_Cvars.erase(iter); - - delete info; - } - - m_Cache.clear(); - m_HookDetour->Destroy(); + OnAmxxShutdown(); } void CvarManager::CreateCvarHook(void) @@ -103,8 +120,7 @@ cvar_t* CvarManager::CreateCvar(const char* name, const char* value, float fvalu cvar_t* var = nullptr; CvarInfo* info = nullptr; - // Is cvar already cached ? - if (!m_Cache.retrieve(name, &info)) + if (!CacheLookup(name, &info)) { // Not cached - Is cvar already exist? var = CVAR_GET_POINTER(name); @@ -174,7 +190,7 @@ cvar_t* CvarManager::FindCvar(const char* name) CvarInfo* info = nullptr; // Do we have already cvar in cache? - if (m_Cache.retrieve(name, &info)) + if (CacheLookup(name, &info)) { return info->var; } @@ -218,6 +234,55 @@ CvarInfo* CvarManager::FindCvar(size_t index) return nullptr; } +bool CvarManager::CacheLookup(const char* name, CvarInfo** info) +{ + return m_Cache.retrieve(name, info); +} + +Forward* CvarManager::HookCvarChange(cvar_t* var, AMX* amx, cell param, const char** callback) +{ + CvarInfo* info = nullptr; + + // A cvar is guaranteed to be in cache if pointer is got from + // get_cvar_pointer and register_cvar natives. Though it might be + // provided by another way. If by any chance we run in such + // situation, we create a new entry right now. + + if (!CacheLookup(var->name, &info)) + { + // Create a new entry. + info = new CvarInfo(); + info->var = var; + info->name = var->name; + info->plugin = ""; + info->pluginId = -1; + info->amxmodx = false; + + // Add entry in the caches. + m_Cvars.append(info); + m_Cache.insert(info->name.chars(), info); + } + + int length; + *callback = get_amxstring(amx, param, 0, length); + + int forwardId = registerSPForwardByName(amx, *callback, FP_CELL, FP_STRING, FP_STRING, FP_DONE); + + // Invalid callback, it could be: not a public function, wrongly named, or simply missing. + if (forwardId == -1) + { + return nullptr; + } + + // Detour is disabled on map change. + m_HookDetour->EnableDetour(); + + Forward* forward = new Forward(forwardId, *callback); + info->hooks.append(new CvarPlugin(g_plugins.findPlugin(amx)->getId(), forward)); + + return forward; +} + size_t CvarManager::GetRegCvarsCount() { return m_AmxmodxCvars; @@ -246,3 +311,38 @@ void CvarManager::OnConsoleCommand() } } } + +void CvarManager::OnPluginUnloaded() +{ + // Clear only plugin hooks list. + for (CvarsList::iterator cvar = m_Cvars.begin(); cvar != m_Cvars.end(); cvar++) + { + for (size_t i = 0; i < (*cvar)->hooks.length(); ++i) + { + delete (*cvar)->hooks[i]; + } + + (*cvar)->hooks.clear(); + } + + // There is no point to enable detour if at next map change + // no plugins hook cvars. + m_HookDetour->DisableDetour(); +} + +void CvarManager::OnAmxxShutdown() +{ + // Free everything. + for (CvarsList::iterator cvar = m_Cvars.begin(); cvar != m_Cvars.end(); cvar = m_Cvars.erase(cvar)) + { + for (size_t i = 0; i < (*cvar)->hooks.length(); ++i) + { + delete (*cvar)->hooks[i]; + } + + delete (*cvar); + } + + m_Cache.clear(); + m_HookDetour->Destroy(); +} diff --git a/amxmodx/cvars.h b/amxmodx/cvars.h index e4b71891..a5eb7eb7 100644 --- a/amxmodx/cvars.h +++ b/amxmodx/cvars.h @@ -10,12 +10,46 @@ #ifndef CVARS_H #define CVARS_H -#include "cvardef.h" +#include "amxmodx.h" +#include #include #include class CDetour; +struct Forward +{ + enum fwdstate + { + FSTATE_INVALID = 0, + FSTATE_OK, + FSTATE_STOP, + }; + + Forward(int id_, const char* handler) : id(id_), state(FSTATE_OK), callback(handler) {}; + Forward() : id(-1) , state(FSTATE_INVALID) {}; + + ~Forward() + { + unregisterSPForward(id); + } + + int id; + fwdstate state; + ke::AString callback; +}; + +struct CvarPlugin +{ + CvarPlugin(int id, Forward* fwd) : pluginId(id), forward(fwd) {}; + CvarPlugin(int id) : pluginId(id), forward(new Forward()) {}; + + int pluginId; + ke::AutoPtr forward; +}; + +typedef ke::Vector CvarsHook; + struct CvarInfo : public ke::InlineListNode { cvar_t* var; @@ -23,6 +57,7 @@ struct CvarInfo : public ke::InlineListNode ke::AString defaultval; ke::AString plugin; int pluginId; + CvarsHook hooks; bool amxmodx; static inline bool matches(const char *name, const CvarInfo* info) @@ -44,11 +79,19 @@ class CvarManager public: void CreateCvarHook(); + cvar_t* CreateCvar(const char* name, const char* value, float fvalue, int flags, const char* plugin, int plugnId); cvar_t* FindCvar(const char* name); CvarInfo* FindCvar(size_t index); + bool CacheLookup(const char* name, CvarInfo** info); + + Forward* HookCvarChange(cvar_t* var, AMX* amx, cell param, const char** callback); + size_t GetRegCvarsCount(); + void OnConsoleCommand(); + void OnPluginUnloaded(); + void OnAmxxShutdown(); private: diff --git a/amxmodx/meta_api.cpp b/amxmodx/meta_api.cpp index fc4bc89d..82fae683 100755 --- a/amxmodx/meta_api.cpp +++ b/amxmodx/meta_api.cpp @@ -697,6 +697,9 @@ void C_ServerDeactivate_Post() g_vault.clear(); g_xvars.clear(); g_plugins.clear(); + + g_CvarManager.OnPluginUnloaded(); + ClearPluginLibraries(); modules_callPluginsUnloaded(); diff --git a/plugins/include/cvars.inc b/plugins/include/cvars.inc index 3e8dcc62..c5803aed 100644 --- a/plugins/include/cvars.inc +++ b/plugins/include/cvars.inc @@ -13,12 +13,40 @@ #define _cvars_included /** - * Called when a console variable's value is changed. - * - * @param cvarHandle Handle to the cvar that was changed - * @param oldValue String containing the value of the cvar before it was changed - * @param newValue String containing the new value of the cvar - * @param cvarName String containing the name of the cvar + * Creates a hook for when a console variable's value is changed. * - * public OnCvarChanged(cvarHandle, const oldValue[], const newValue[], const cvarName[]); + * @note Callback will be called in the following manner: + * + * public cvar_change_callback(pcvar, const old_value[], const new_value[]) + * + * pcvar - Pointer to cvar that was changed + * old_value - String containing the value of the cvar before it was changed + * new_value - String containing the new value of the cvar + * + * @param pcvar Pointer to cvar + * @param callback Name of callback function + * @error Invalid pointer or invalid callback function */ +native cvarhook:hook_cvar_change(pcvar, const callback[]); + +/** + * Stops a cvar hook forward from triggering. + * + * @note Use the return value from hook_cvar_change as the parameter here. + * + * @param handle Forward to stop + * @error Invalid hook handle + */ +native disable_cvar_hook(cvarhook:handle); + +/** + * Starts a cvar hook forward back up. + * + * @note Use the return value from hook_cvar_change as the parameter here. + * + * @param handle Forward to back up + * @error Invalid hook handle + */ +native enable_cvar_hook(cvarhook:handle); + +