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():
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;
}CConVar myplugin_enabled;
CConVar myplugin_bonus;
CConVar myplugin_scale;
public void OnPluginStart()
{
myplugin_enabled = CConVar.CreateBool("myplugin_enabled", FCVAR_NONE, "Turns the plugin on and off", true);
myplugin_bonus = CConVar.CreateInt("myplugin_bonus", FCVAR_NONE, "Money given at round start", 1337);
myplugin_scale = CConVar.CreateFloat("myplugin_scale", FCVAR_NONE, "Damage multiplier",
1.0, true, 0.0, true, 10.0); // min 0, max 10
}g_pCVarhas to be set beforeConVar_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()andSet():if (myplugin_enabled.Get()) …,myplugin_bonus.Set(500);. - A script needs neither
g_pCVarnorConVar_Register(), and reads and writes through the handle:myplugin_enabled.BoolValue,myplugin_bonus.IntValue = 500;,FloatValue,GetString()/SetString().
Constructors
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:
| Flag | Meaning |
|---|---|
FCVAR_NONE | Nothing special. |
FCVAR_REPLICATED | The server's value is sent to clients and enforced there (movement, prediction). See Replication. |
FCVAR_NOTIFY | Players are told in chat when it changes. |
FCVAR_PROTECTED | A password or similar: the value is not sent out. |
FCVAR_CHEAT | Only changeable with sv_cheats 1. |
FCVAR_HIDDEN | Not listed by find or autocomplete. |
FCVAR_RELEASE | Available 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:
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);
});CConVar myplugin_mode;
public void OnPluginStart()
{
myplugin_mode = CConVar.CreateInt("myplugin_mode", FCVAR_NONE, "0 = off, 1 = on, 2 = debug", 1);
myplugin_mode.AddChangeHook(OnModeChanged);
}
public void OnModeChanged(ConVar convar, const char[] oldValue, const char[] newValue)
{
Source2ModApi.Log("myplugin_mode: %s -> %s", oldValue, newValue);
}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:
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
CConVarRef<int> mp_limitteams("mp_limitteams");
if (mp_limitteams.IsValidRef() && mp_limitteams.IsConVarDataAvailable())
{
int current = mp_limitteams.Get();
mp_limitteams.Set(5);
}CConVarRef mp_limitteams = CConVarRef.Find("mp_limitteams");
if (mp_limitteams != null && mp_limitteams.IsValidRef() && mp_limitteams.IsConVarDataAvailable())
{
int current = mp_limitteams.IntValue;
mp_limitteams.IntValue = 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. CheckIsConVarDataAvailable(). - 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);// No by-name helpers: find the ConVar, check it, set it on the handle.
CConVarRef cvar;
if ((cvar = CConVarRef.Find("mp_freezetime")) != null)
cvar.IntValue = 5;
if ((cvar = CConVarRef.Find("mp_roundtime")) != null)
cvar.FloatValue = 1.92;
if ((cvar = CConVarRef.Find("hostname")) != null)
cvar.SetString("My Server");
if ((cvar = CConVarRef.Find("sv_jump_impulse")) != null)
cvar.Flags &= ~FCVAR_CHEAT; // settable without sv_cheats
if ((cvar = CConVarRef.Find("myplugin_bonus")) != null)
cvar.Flags |= 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);// The ConVar handle is what the access index is in C++.
ConVar mp_roundtime = CConVarRef.Find("mp_roundtime");
float roundTime = mp_roundtime.FloatValue;
mp_roundtime.FloatValue = 2.5;
EConVarType type = Source2ModConVars.GetType(mp_roundtime);
int flags[2]; // all 64 bits, flags[0] the low half
Source2ModConVars.GetFlags(mp_roundtime, flags);
char help[256];
mp_roundtime.GetDescription(help, sizeof(help));| Group | Methods |
|---|---|
| Lookup | GetConvarAccessIndexByName, GetConvarRef, GetName, GetHelpText, GetType |
| Flags | GetFlags, SetFlags (replaces the whole set) |
| Scalars | GetString/Bool/Int/Float/Double, SetString/Bool/Int/Float/Double |
| Vectors | GetVector2/3/4, GetQAngle, GetColor and their Set… |
| Raw | GetValueAddress, 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):
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()CConVar dynamic = CConVar.Create("myplugin_dynamic", EConVarType_Float32, FCVAR_NONE, "Created at runtime",
"1.0", true, "0.0", true, "10.0");
// ...
Source2ModConVars.DeleteConVar(dynamic);defaultValue,minValueandmaxValuepoint to a value of the matching type (int32forEConVarType_Int32,floatforEConVarType_Float32, …). ForEConVarType_Stringpass the string itself,(void*)"text", not a pointer to it.- Pass valid
minValue/maxValuepointers even whenhasMin/hasMaxare 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.
DeleteConVarinUnload()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.
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);
});public void OnPluginStart()
{
Source2ModConVars.HookConVarChange(OnConVarChanged);
}
public void OnConVarChanged(ConVar convar, int slot, const char[] newValue, const char[] oldValue)
{
char name[64];
convar.GetName(name, sizeof(name));
if (!Source2ModUtils.Equal(name, "sv_autobunnyhopping"))
return;
Source2ModApi.Log("%s changed from %s to %s", name, oldValue, newValue);
}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));int id = Source2ModConVars.HookConVarChange(OnConVarChanged);
// ...
Source2ModConVars.UnhookConVarChange(id);See also
- Replication — a different value per player.
ISource2ModConVarsreference