ConVars

Declaring your own console variables, reading and writing the game's, and reacting when any of them changes.

ConVars in CS2 are s2sdk's CConVar<T> and CConVarRef<T> — Source2Mod does not wrap them in a class of its own. What it adds is ISource2ModConVars: an index-based interface for reading, writing and creating ConVars without templates, a process-wide change callback that takes lambdas, and a few by-name helpers. Scripts get the same through the CConVar and CConVarRef methodmaps and Source2ModConVars (source2mod/convars.inc).

Declaring your own

In C++, declare a CConVar<T> at file scope, then register the library's ConVars once in Load(). A script creates each one with CConVar.Create* in OnPluginStart():

plugin.cpp
CConVar<bool>  myplugin_enabled("myplugin_enabled", FCVAR_NONE, "Turns the plugin on and off", true);
CConVar<int>   myplugin_bonus("myplugin_bonus", FCVAR_NONE, "Money given at round start", 1337);
CConVar<float> myplugin_scale("myplugin_scale", FCVAR_NONE, "Damage multiplier",
                              1.0f, true, 0.0f, true, 10.0f); // min 0, max 10

bool MyPlugin::Load(PluginId id, ISource2ModAPI* api, char* error, size_t maxlen, bool late)
{
    SOURCE2MOD_SAVEVARS();

    g_pCVar = api->GetCVar();

    ConVar_Register(FCVAR_RELEASE | FCVAR_CLIENT_CAN_EXECUTE | FCVAR_GAMEDLL);
    return true;
}
  • g_pCVar has to be set before ConVar_Register() — that is where the registration goes.
  • The flags passed to ConVar_Register() are added to every ConVar and ConCommand declared in your library. The set above is what the sample plugin and the core use.
  • Read and write your own ConVar with Get() and Set(): if (myplugin_enabled.Get()) …, myplugin_bonus.Set(500);.
  • A script needs neither g_pCVar nor ConVar_Register(), and reads and writes through the handle: myplugin_enabled.BoolValue, myplugin_bonus.IntValue = 500;, FloatValue, GetString()/SetString().

Constructors

tier1/convar.h (s2sdk)
CConVar(const char* name, uint64 flags, const char* help_string, const T& default_value,
        FnTypedChangeCallback_t<T> cb = nullptr, ...);

CConVar(const char* name, uint64 flags, const char* help_string, const T& default_value,
        bool min, const T& minValue, bool max, const T& maxValue,
        FnTypedChangeCallback_t<T> cb = nullptr, ...);

Typical T: bool, int, float, double, CUtlString, Vector, QAngle, Color.

Flags

The FCVAR_* flags come from s2sdk's convar.h. The ones you will use:

FlagMeaning
FCVAR_NONENothing special.
FCVAR_REPLICATEDThe server's value is sent to clients and enforced there (movement, prediction). See Replication.
FCVAR_NOTIFYPlayers are told in chat when it changes.
FCVAR_PROTECTEDA password or similar: the value is not sent out.
FCVAR_CHEATOnly changeable with sv_cheats 1.
FCVAR_HIDDENNot listed by find or autocomplete.
FCVAR_RELEASEAvailable in release builds — part of the usual ConVar_Register flags.

Change callback on your own ConVar

The last constructor argument is a typed callback, a plain function pointer (a lambda without captures converts to one). A script calls AddChangeHook on its ConVar instead and gets the old and new value as text:

plugin.cpp
CConVar<int> myplugin_mode("myplugin_mode", FCVAR_NONE, "0 = off, 1 = on, 2 = debug", 1,
    [](CConVar<int>* cvar, CSplitScreenSlot slot, const int* pNewValue, const int* pOldValue)
    {
        SOURCE2MOD_CONPRINTF("myplugin_mode: %d -> %d\n", *pOldValue, *pNewValue);
    });

When you need captures, or want to hear about ConVars that are not yours, use HOOK_CONVAR_CHANGE instead.

Unloading

Every C++ plugin that declares a CConVar must call ConVar_Unregister() in Unload().

ConVar_Register() hands the engine pointers to the CConVar objects in your library. When the library is unloaded those pointers dangle, and the next thing that touches one — autocomplete, an exec — reads freed memory. ConVar_Unregister() only walks the list of the library that calls it, so the Source2Mod cannot do this for you:

plugin.cpp
bool MyPlugin::Unload(char* error, size_t maxlen)
{
    KHOOK_DESTRUCT();
    ConVar_Unregister();
    return true;
}

C++ only — a script has nothing to unregister: its CConVar belongs to the core.

Reading and writing the game's ConVars

CConVarRef<T> — typed, for a ConVar you keep using

plugin.cpp
CConVarRef<int> mp_limitteams("mp_limitteams");

if (mp_limitteams.IsValidRef() && mp_limitteams.IsConVarDataAvailable())
{
    int current = mp_limitteams.Get();
    mp_limitteams.Set(5);
}

In a script, CConVarRef.Find returns null for a name nobody registered, and the handle is not typed: IntValue, FloatValue, BoolValue and GetString() convert. The list below is about C++'s CConVarRef<T>.

Things the sample plugin spells out, all worth knowing:

  • A CConVarRef<T> to a name that is not registered yet pre-registers it and reads default data until the real ConVar appears. Check IsConVarDataAvailable().
  • The type must be right. A ref of the wrong type can keep the real ConVar from registering, or leave you with an invalid ref — check IsValidRef() before reading or writing. An invalid ref reads and writes shared dummy data.
  • Get()/Set() are the fast typed path. GetFloat(), GetBool(), GetString(), SetFloat(), … convert between types when possible.

A ref to a file-scope static CConVarRef<bool> sv_autobunnyhopping("sv_autobunnyhopping"); is a common pattern in FUNPLAY's plugins.

ConVarRefAbstract — untyped

ConVarRefAbstract ref("mp_limitteams");

if (ref.IsValidRef())
{
    ref.SetFloat(10.0f);                        // converting setters
    float f = ref.GetFloat();
    ref.SetAs<Vector>(Vector(1.0f, 2.0f, 3.0f)); // for non-primitive types

    CConVarRef<int> typed(ref);                 // "promote" to a typed ref
    if (typed.IsValidRef() && typed.IsConVarDataValid())
        typed.Set(5);
}

ConVarRefAbstract does not pre-register anything; it is a plain reference.

C++ only — a script's ConVar handle is untyped to begin with: its accessors convert, and Source2ModConVars.SetVector3, SetQAngle, SetColor, … write the non-primitive types.

One-shot helpers by name

ISource2ModConVars.h has three inline helpers for "change this and move on". They look the ConVar up themselves and return false if it does not exist. A script has no by-name helpers; it finds the ConVar and sets it on the handle (ConVar.Flags holds the low 32 flag bits, Source2ModConVars.GetFlags / SetFlags all 64):

UTIL_SetConVar("mp_freezetime", 5);
UTIL_SetConVar<float>("mp_roundtime", 1.92f);
UTIL_SetConVar<const char*>("hostname", "My Server");

UTIL_RemoveConVarFlags("sv_jump_impulse", FCVAR_CHEAT); // settable without sv_cheats
UTIL_AddConVarFlags("myplugin_bonus", FCVAR_NOTIFY);

For UTIL_SetConVar the type decides how the value is written, so spell it out (UTIL_SetConVar<int>(…)) when a literal would deduce to the wrong one — a string literal always needs <const char*>. These go through s2sdk directly, so they also work before Source2Mod's ConVar manager is up.

ISource2ModConVars — by access index

Every ConVar has a 16-bit access index. The interface is built around it, which suits code that does not know the type at compile time. In a script the ConVar handle takes the index's place:

uint16 idx = CVAR_IDX("mp_roundtime");        // GetConvarAccessIndexByName

float roundTime = CVAR_GET_FLOAT(idx);
CVAR_SET_FLOAT(idx, 2.5f);

EConVarType type = g_pSource2ModConVars->GetType(idx);
uint64 flags     = g_pSource2ModConVars->GetFlags(idx);
const char* help = g_pSource2ModConVars->GetHelpText(idx);
GroupMethods
LookupGetConvarAccessIndexByName, GetConvarRef, GetName, GetHelpText, GetType
FlagsGetFlags, SetFlags (replaces the whole set)
ScalarsGetString/Bool/Int/Float/Double, SetString/Bool/Int/Float/Double
VectorsGetVector2/3/4, GetQAngle, GetColor and their Set…
RawGetValueAddress, GetValue, SetValue

Macros: CVAR_IDX, CVAR_REF, CVAR_GET_STR/BOOL/INT/FLOAT/DOUBLE, CVAR_SET_STR/BOOL/INT/FLOAT/DOUBLE, CVAR_CREATE, CVAR_DELETE. Every getter and setter takes an optional last CSplitScreenSlot (default -1), which you can leave alone on a server.

GetConvarAccessIndexByName returns SOURCE2MOD_INVALID_CONVAR_INDEX (0xFFFF) when the name does not exist. Check for it before reading or writing -- 0 is a valid index, so do not test against that.

GetString returns a pointer into a thread-local buffer: copy it before the next GetString call if you need to keep it.

Creating ConVars at runtime

When the name is only known at runtime — read from a config, say — create the ConVar through the interface (a script: CConVar.Create, the values as text — "1 2 3" for a vector, "255 0 0 255" for a colour):

ISource2ModConVars.h
virtual uint16 CreateConVar(const char* name, EConVarType type, const char* help, uint64 flags,
                            bool hasMin, bool hasMax,
                            void* defaultValue, const void* minValue, const void* maxValue) = 0;
virtual void DeleteConVar(uint16 accessIndex) = 0;
float def = 1.0f, min = 0.0f, max = 10.0f;
uint16 idx = CVAR_CREATE("myplugin_dynamic", EConVarType_Float32, "Created at runtime",
                         FCVAR_NONE, true, true, &def, &min, &max);

// ...

CVAR_DELETE(idx); // in Unload()
  • defaultValue, minValue and maxValue point to a value of the matching type (int32 for EConVarType_Int32, float for EConVarType_Float32, …). For EConVarType_String pass the string itself, (void*)"text", not a pointer to it.
  • Pass valid minValue/maxValue pointers even when hasMin/hasMax are false — for numeric types they are read either way.
  • If a ConVar with that name exists already, nothing is created and its index is returned.
  • The ConVar is created by the core, not by your library, and is not removed when your plugin unloads. DeleteConVar in Unload() is on you.

For ConVars known at compile time, a CConVar<T> is simpler and needs no cleanup beyond ConVar_Unregister().

Watching changes

HOOK_CONVAR_CHANGE registers a callback for every ConVar change on the server. The engine only takes one plain function pointer for this; Source2Mod installs it once and fans out, so your handler can be a capturing lambda, and it is dropped automatically when the plugin unloads. A script passes a named public function to Source2ModConVars.HookConVarChange.

plugin.cpp
HOOK_CONVAR_CHANGE([](ConVarRefAbstract* ref, CSplitScreenSlot slot, const char* pszNewValue, const char* pszOldValue)
{
    if (V_strcmp(ref->GetName(), "sv_autobunnyhopping") != 0)
        return;

    SOURCE2MOD_LOG(&g_Plugin, "%s changed from %s to %s\n", ref->GetName(), pszOldValue, pszNewValue);
});
ISource2ModConVars.h
using ConVarChangeHandler = Source2ModCallback<void(ConVarRefAbstract* ref, CSplitScreenSlot slot,
                                                  const char* pszNewValue, const char* pszOldValue)>;

virtual Source2ModHookId HookConVarChange(ConVarChangeHandler handler) = 0;
virtual bool UnhookConVarChange(const ConVarChangeHandler& handler) = 0;
virtual bool UnhookConVarChange(Source2ModHookId id) = 0;

Filter by name first — the callback fires for all ConVars, the game's included. A plugin may register as many handlers as it likes. There is no plugin id to pass: the handler is a lambda, a function or an object and a method, and the core reads the owner off it. HOOK_CONVAR_CHANGE(handler) returns a Source2ModHookId; to drop one handler while the plugin stays loaded, pass that id to g_pSource2ModConVars->UnhookConVarChange(id), or pass the same function or object and method again to UNHOOK_CONVAR_CHANGE(handler) (a lambda only goes by id). A script unhooks by id only, with Source2ModConVars.UnhookConVarChange(id):

HOOK_CONVAR_CHANGE(SOURCE2MOD_MEMBER(this, &MyPlugin::OnConVarChanged));
// ...
UNHOOK_CONVAR_CHANGE(SOURCE2MOD_MEMBER(this, &MyPlugin::OnConVarChanged));

See also

On this page