diff --git a/addons/source-python/packages/source-python/cvars/__init__.py b/addons/source-python/packages/source-python/cvars/__init__.py old mode 100644 new mode 100755 index 66177aede..516a2a380 --- a/addons/source-python/packages/source-python/cvars/__init__.py +++ b/addons/source-python/packages/source-python/cvars/__init__.py @@ -6,6 +6,8 @@ # >> IMPORTS # ============================================================================= # Source.Python Imports +# Core +from core import AutoUnload # Cvars from _cvars import ConVar from _cvars import _Cvar @@ -24,5 +26,68 @@ # >> ALL DECLARATION # ============================================================================= __all__ = ('ConVar', + 'ConVarChanged', 'cvar', ) + + +# ============================================================================= +# >> CLASSES +# ============================================================================= +class ConVarChanged(AutoUnload): + """ConVarChanged decorator class.""" + + def __init__(self, *convars): + """Store the convars.""" + self._convars = () + self.callback = None + + # Validate convars + if not convars: + raise ValueError('At least one convar is required.') + + _convars = [] + for convar in convars: + if not isinstance(convar, (str, ConVar)): + raise ValueError('Given convar is not ConVar or ConVar name.') + + elif isinstance(convar, str): + convar_name = convar + convar = cvar.find_var(convar_name) + if convar is None: + raise ValueError( + f'"{convar_name}" is not a valid ConVar name.') + + _convars.append(convar) + + self._convars = tuple(_convars) + + def __call__(self, callback): + """Store the callback and add it to convars.""" + # Is the callback callable? + if not callable(callback): + raise ValueError('Given callback is not callable.') + + # Store the callback + self.callback = callback + + # Loop through all convars + for convar in self._convars: + + # Add the callback + convar.add_changed_callback(self.callback) + + # Return the callback + return self.callback + + def _unload_instance(self): + """Remove the callback from convars.""" + # Was no callback registered? + if self.callback is None: + return + + # Loop through all convars + for convar in self._convars: + + # Remove the callback + convar.remove_changed_callback(self.callback) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt old mode 100644 new mode 100755 index 9bb21297a..889a15441 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -171,6 +171,7 @@ Set(SOURCEPYTHON_CVARS_MODULE_HEADERS ) Set(SOURCEPYTHON_CVARS_MODULE_SOURCES + core/modules/cvars/cvars.cpp core/modules/cvars/cvars_wrap.cpp ) diff --git a/src/core/modules/cvars/cvars.cpp b/src/core/modules/cvars/cvars.cpp new file mode 100755 index 000000000..0dc945a32 --- /dev/null +++ b/src/core/modules/cvars/cvars.cpp @@ -0,0 +1,217 @@ +/** +* ============================================================================= +* Source Python +* Copyright (C) 2012-2015 Source Python Development Team. All rights reserved. +* ============================================================================= +* +* This program is free software; you can redistribute it and/or modify it under +* the terms of the GNU General Public License, version 3.0, as published by the +* Free Software Foundation. +* +* This program is distributed in the hope that it will be useful, but WITHOUT +* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +* details. +* +* You should have received a copy of the GNU General Public License along with +* this program. If not, see . +* +* As a special exception, the Source Python Team gives you permission +* to link the code of this program (as well as its derivative works) to +* "Half-Life 2," the "Source Engine," and any Game MODs that run on software +* by the Valve Corporation. You must obey the GNU General Public License in +* all respects for all other code used. Additionally, the Source.Python +* Development Team grants this exception to all derivative works. +*/ + +//----------------------------------------------------------------------------- +// Includes. +//----------------------------------------------------------------------------- +#include "cvars.h" + + +//----------------------------------------------------------------------------- +// Global ConVar changed callback mapping. +//----------------------------------------------------------------------------- +typedef std::vector ChangedCallbacks; +typedef std::unordered_map ConVarMap; +ConVarMap g_ConVarMap; + + +//----------------------------------------------------------------------------- +// ConVar extension class. +//----------------------------------------------------------------------------- +boost::shared_ptr ConVarExt::__init__(const char* name, const char* value, + const char* description, int flags, object min_value, object max_value) +{ + if (!name || name[0] == '\0') + BOOST_RAISE_EXCEPTION(PyExc_ValueError, "An empty string is not a valid ConVar name.") + + float fMin = 0; + float fMax = 0; + + try { + fMin = extract(min_value); + } + catch (...) { + PyErr_Clear(); + } + + try { + fMax = extract(max_value); + } + catch (...) { + PyErr_Clear(); + } + + ConVar *pConVar = g_pCVar->FindVar(name); + if (!pConVar) + { + // Find if the command already exists + ConCommand* pConCommand = g_pCVar->FindCommand(name); + if (pConCommand) + BOOST_RAISE_EXCEPTION(PyExc_ValueError, "ConCommand already exists.") + + ConVar* pConVar = new ConVar(strdup(name), strdup(value), flags, + strdup(description), !min_value.is_none(), fMin, !max_value.is_none(), fMax); + + return boost::shared_ptr(pConVar, &NeverDeleteDeleter); + } + + return boost::shared_ptr(pConVar, &NeverDeleteDeleter); +} + +bool ConVarExt::HasMin(ConVar* pConVar) +{ + float fMin; + return pConVar->GetMin(fMin); +} + +bool ConVarExt::HasMax(ConVar* pConVar) +{ + float fMax; + return pConVar->GetMax(fMax); +} + +float ConVarExt::GetMin(ConVar* pConVar) +{ + float fMin; + pConVar->GetMin(fMin); + return fMin; +} + +float ConVarExt::GetMax(ConVar* pConVar) +{ + float fMax; + pConVar->GetMax(fMax); + return fMax; +} + +void ConVarExt::SetValue(ConVar* pConVar, bool bValue) +{ + pConVar->SetValue(bValue); +} + +void ConVarExt::MakePublic(ConVar* pConVar) +{ + pConVar->m_nFlags |= FCVAR_NOTIFY; + g_pCVar->CallGlobalChangeCallbacks(pConVar, pConVar->GetString(), pConVar->GetFloat()); +} + +void ConVarExt::RemovePublic(ConVar* pConVar) +{ + pConVar->m_nFlags &= ~FCVAR_NOTIFY; + g_pCVar->CallGlobalChangeCallbacks(pConVar, pConVar->GetString(), pConVar->GetFloat()); +} + +void ConVarExt::ChangedCallback(IConVar* var, const char* pOldValue, float flOldValue) +{ + ConVarMap::iterator map_it = g_ConVarMap.find(var->GetName()); + if (map_it == g_ConVarMap.end()) + return; + + ConVar* pConVar = static_cast(var); + + ChangedCallbacks& callables = map_it->second; + for (ChangedCallbacks::iterator it = callables.begin(); it != callables.end(); ++it) + { + BEGIN_BOOST_PY() + (*it)(ptr(pConVar), pOldValue, pConVar->GetString()); + END_BOOST_PY_NORET() + } +} + +void ConVarExt::AddChangedCallback(ConVar* pConVar, PyObject* pCallable) +{ + // Get the object instance of the callable + object oCallable = object(handle<>(borrowed(pCallable))); + + ChangedCallbacks& callables = g_ConVarMap[pConVar->GetName()]; + if (!callables.size()) + { + if (!installed) + { + g_pCVar->InstallGlobalChangeCallback(ChangedCallback); + installed = true; + } + } + else + { + for (ChangedCallbacks::iterator it = callables.begin(); it != callables.end(); ++it) + { + if (is_same_func(oCallable, *it)) + BOOST_RAISE_EXCEPTION(PyExc_ValueError, "Callback already registered.") + } + } + + callables.push_back(oCallable); +} + +void ConVarExt::RemoveChangedCallback(ConVar* pConVar, PyObject* pCallable) +{ + ConVarMap::iterator map_it = g_ConVarMap.find(pConVar->GetName()); + if (map_it == g_ConVarMap.end()) + BOOST_RAISE_EXCEPTION(PyExc_ValueError, "Callback not registered.") + + // Get the object instance of the callable + object oCallable = object(handle<>(borrowed(pCallable))); + + ChangedCallbacks& callables = map_it->second; + for (ChangedCallbacks::iterator it = callables.begin();;) + { + if(it == callables.end()) + BOOST_RAISE_EXCEPTION(PyExc_ValueError, "Callback not registered.") + + if (is_same_func(oCallable, *it)) + { + callables.erase(it); + break; + } + else + { + ++it; + } + } + + if (!callables.size()) + { + g_ConVarMap.erase(map_it); + if (!g_ConVarMap.size()) + { + g_pCVar->RemoveGlobalChangeCallback(ChangedCallback); + installed = false; + } + } +} + +void ConVarExt::ClearCallback() +{ + if (installed) + { + g_ConVarMap.clear(); + g_pCVar->RemoveGlobalChangeCallback(ChangedCallback); + installed = false; + } +} + +bool ConVarExt::installed = false; diff --git a/src/core/modules/cvars/cvars.h b/src/core/modules/cvars/cvars.h old mode 100644 new mode 100755 index bfa2079f2..8c9f1e4bb --- a/src/core/modules/cvars/cvars.h +++ b/src/core/modules/cvars/cvars.h @@ -30,8 +30,17 @@ //----------------------------------------------------------------------------- // Includes. //----------------------------------------------------------------------------- +// C++ +#include +#include + +// This is required for accessing m_nFlags without patching convar.h +#define private public #include "convar.h" +#undef private + #include "utilities/sp_util.h" +#include "modules/listeners/listeners_manager.h" //----------------------------------------------------------------------------- @@ -41,82 +50,25 @@ class ConVarExt { public: static boost::shared_ptr __init__(const char* name, const char* value, - const char* description, int flags, object min_value, object max_value) - { - if (!name || name[0] == '\0') - BOOST_RAISE_EXCEPTION(PyExc_ValueError, "An empty string is not a valid ConVar name.") - - float fMin = 0; - float fMax = 0; - - try { - fMin = extract(min_value); - } - catch (...) { - PyErr_Clear(); - } - - try { - fMax = extract(max_value); - } - catch (...) { - PyErr_Clear(); - } - - ConVar *pConVar = g_pCVar->FindVar(name); - if (!pConVar) - { - ConVar* pConVar = new ConVar(strdup(name), strdup(value), flags, - strdup(description), !min_value.is_none(), fMin, !max_value.is_none(), fMax); - - return boost::shared_ptr(pConVar, &NeverDeleteDeleter); - } - - return boost::shared_ptr(pConVar, &NeverDeleteDeleter); - } - - static bool HasMin(ConVar* pConVar) - { - float fMin; - return pConVar->GetMin(fMin); - } + const char* description, int flags, object min_value, object max_value); - static bool HasMax(ConVar* pConVar) - { - float fMax; - return pConVar->GetMax(fMax); - } + static bool HasMin(ConVar* pConVar); + static bool HasMax(ConVar* pConVar); - static float GetMin(ConVar* pConVar) - { - float fMin; - pConVar->GetMin(fMin); - return fMin; - } + static float GetMin(ConVar* pConVar); + static float GetMax(ConVar* pConVar); - static bool GetMax(ConVar* pConVar) - { - float fMax; - pConVar->GetMax(fMax); - return fMax; - } + static void SetValue(ConVar* pConVar, bool bValue); - static void SetValue(ConVar* pConVar, bool bValue) - { - pConVar->SetValue(bValue); - } + static void MakePublic(ConVar* pConVar); + static void RemovePublic(ConVar* pConVar); - static void MakePublic(ConVar* pConVar) - { - pConVar->m_nFlags |= FCVAR_NOTIFY; - g_pCVar->CallGlobalChangeCallbacks(pConVar, pConVar->GetString(), pConVar->GetFloat()); - } + static void ChangedCallback(IConVar* var, const char* pOldValue, float flOldValue); + static void AddChangedCallback(ConVar* pConVar, PyObject* pCallable); + static void RemoveChangedCallback(ConVar* pConVar, PyObject* pCallable); + static void ClearCallback(); - static void RemovePublic(ConVar* pConVar) - { - pConVar->m_nFlags &= ~FCVAR_NOTIFY; - g_pCVar->CallGlobalChangeCallbacks(pConVar, pConVar->GetString(), pConVar->GetFloat()); - } + static bool installed; }; diff --git a/src/core/modules/cvars/cvars_wrap.cpp b/src/core/modules/cvars/cvars_wrap.cpp old mode 100644 new mode 100755 index 997d410f6..f4d66b954 --- a/src/core/modules/cvars/cvars_wrap.cpp +++ b/src/core/modules/cvars/cvars_wrap.cpp @@ -299,7 +299,19 @@ void export_convar(scope _cvars) &ConVarExt::RemovePublic, "Remove the notify flag and make the console variable no longer public." ) - + + .def("add_changed_callback", + &ConVarExt::AddChangedCallback, + "Add a callable object that will be called when the ConVar is changed.", + args("callable") + ) + + .def("remove_changed_callback", + &ConVarExt::RemoveChangedCallback, + "Remove a callable object that will be called when the ConVar is changed.", + args("callable") + ) + // Special methods... .def("__float__", &ConVar::GetFloat, diff --git a/src/core/sp_main.cpp b/src/core/sp_main.cpp index 6f1be1dcb..5b0fdcf1b 100755 --- a/src/core/sp_main.cpp +++ b/src/core/sp_main.cpp @@ -58,6 +58,7 @@ #include "manager.h" #include "modules/listeners/listeners_manager.h" +#include "modules/cvars/cvars.h" #include "utilities/conversions.h" #include "modules/entities/entities_entity.h" #include "modules/core/core.h" @@ -403,6 +404,9 @@ void CSourcePython::Unload( void ) DevMsg(1, MSG_PREFIX "Clearing convar changed listener...\n"); GetOnConVarChangedListenerManager()->clear(); + DevMsg(1, MSG_PREFIX "Clearing convar changed callbacks...\n"); + ConVarExt::ClearCallback(); + DevMsg(1, MSG_PREFIX "Unhooking all functions...\n"); GetHookManager()->UnhookAllFunctions();