Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added the ability to add callbacks to ConVar that will be called when ConVar is changed. #421

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
65 changes: 65 additions & 0 deletions addons/source-python/packages/source-python/cvars/__init__.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
# >> IMPORTS
# =============================================================================
# Source.Python Imports
# Core
from core import AutoUnload
# Cvars
from _cvars import ConVar
from _cvars import _Cvar
Expand All @@ -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)
1 change: 1 addition & 0 deletions src/CMakeLists.txt
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -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
)

Expand Down
217 changes: 217 additions & 0 deletions src/core/modules/cvars/cvars.cpp
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*
* 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<object> ChangedCallbacks;
typedef std::unordered_map<std::string, ChangedCallbacks> ConVarMap;
ConVarMap g_ConVarMap;


//-----------------------------------------------------------------------------
// ConVar extension class.
//-----------------------------------------------------------------------------
boost::shared_ptr<ConVar> 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<float>(min_value);
}
catch (...) {
PyErr_Clear();
}

try {
fMax = extract<float>(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<ConVar>(pConVar, &NeverDeleteDeleter<ConVar *>);
}

return boost::shared_ptr<ConVar>(pConVar, &NeverDeleteDeleter<ConVar *>);
}

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<ConVar*>(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;
Loading