/*
* CSB CVar Checker
* Copyright (C) 2026 counter-strike-boost.com
*
* Queries client-side cvars with query_client_cvar a few seconds after a player
* joins and checks each answer against a rules file. Rules can require an exact
* value, forbid a value, enforce a numeric minimum or maximum, or demand that
* the client answers at all (a client that returns "Bad CVAR request" is either
* missing the cvar or blocking the query). Offenders are logged and optionally
* kicked. Handy for catching brightness / fog / interp cheats.
*
* Inspired by the community "CVar Checker" plugins for AMX Mod X. This is an
* independent GPL re-implementation; no original code is reused.
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later
* version. It 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 for details.
*/
#include
new const PLUGIN[] = "CSB CVar Checker"
new const VERSION[] = "1.0.0"
new const AUTHOR[] = "counter-strike-boost.com"
#define MAX_RULES 48
#define MODE_EQ 0 /* value must match exactly */
#define MODE_NEQ 1 /* value must NOT match */
#define MODE_MAX 2 /* numeric value must be <= expected */
#define MODE_MIN 3 /* numeric value must be >= expected */
#define MODE_ANY 4 /* client must simply answer the query */
new g_szRuleCvar[MAX_RULES][32]
new g_szRuleVal[MAX_RULES][32]
new g_iRuleMode[MAX_RULES]
new g_iNumRules
new g_pEnabled, g_pDelay, g_pAction, g_pKickMsg
public plugin_init()
{
register_plugin(PLUGIN, VERSION, AUTHOR)
g_pEnabled = register_cvar("csb_cc_enabled", "1")
g_pDelay = register_cvar("csb_cc_delay", "15")
g_pAction = register_cvar("csb_cc_action", "1") /* 0 = log only, 1 = kick */
g_pKickMsg = register_cvar("csb_cc_kickmsg", "Disallowed client setting detected")
}
public plugin_cfg()
{
loadRules()
}
public client_putinserver(id)
{
if (is_user_bot(id) || is_user_hltv(id) || !get_pcvar_num(g_pEnabled) || g_iNumRules < 1)
return
new Float:delay = get_pcvar_float(g_pDelay)
if (delay < 3.0)
delay = 3.0
set_task(delay, "queryAll", id)
}
public queryAll(id)
{
if (!is_user_connected(id))
return
for (new i = 0; i < g_iNumRules; i++)
query_client_cvar(id, g_szRuleCvar[i], "cvarResult")
}
public cvarResult(id, const cvar[], const value[])
{
if (!is_user_connected(id))
return
new idx = findRule(cvar)
if (idx == -1)
return
new bool:bad = false
new bool:noanswer = equal(value, "Bad CVAR request")
switch (g_iRuleMode[idx])
{
case MODE_EQ: bad = noanswer || !equal(value, g_szRuleVal[idx])
case MODE_NEQ: bad = !noanswer && equal(value, g_szRuleVal[idx])
case MODE_MAX: bad = noanswer || (str_to_float(value) > str_to_float(g_szRuleVal[idx]))
case MODE_MIN: bad = noanswer || (str_to_float(value) < str_to_float(g_szRuleVal[idx]))
case MODE_ANY: bad = noanswer
}
if (!bad)
return
new name[32], authid[35]
get_user_name(id, name, charsmax(name))
get_user_authid(id, authid, charsmax(authid))
log_amx("[CSB CVarCheck] %s <%s> failed rule: %s = '%s' (mode %d, expected '%s')",
name, authid, cvar, value, g_iRuleMode[idx], g_szRuleVal[idx])
if (get_pcvar_num(g_pAction) == 1)
{
new msg[96]
get_pcvar_string(g_pKickMsg, msg, charsmax(msg))
server_cmd("kick #%d ^"%s (%s)^"", get_user_userid(id), msg, cvar)
}
}
findRule(const cvar[])
{
for (new i = 0; i < g_iNumRules; i++)
if (equali(g_szRuleCvar[i], cvar))
return i
return -1
}
loadRules()
{
g_iNumRules = 0
new dir[128]
get_localinfo("amxx_configsdir", dir, charsmax(dir))
new path[160]
formatex(path, charsmax(path), "%s/csb_cvar_checker.ini", dir)
if (!file_exists(path))
writeDefaults(path)
new fp = fopen(path, "rt")
if (!fp)
{
log_amx("[CSB CVarCheck] could not open %s", path)
return
}
new line[128], cvar[32], mode[8], value[32]
while (!feof(fp) && g_iNumRules < MAX_RULES)
{
fgets(fp, line, charsmax(line))
trim(line)
if (!line[0] || line[0] == ';' || (line[0] == '/' && line[1] == '/'))
continue
/* format: cvarname mode value (value optional for "any") */
new pos = 0
pos = readToken(line, pos, cvar, charsmax(cvar))
if (pos == -1) continue
pos = readToken(line, pos, mode, charsmax(mode))
if (pos == -1) continue
readToken(line, pos, value, charsmax(value))
new m
if (equali(mode, "eq")) m = MODE_EQ
else if (equali(mode, "neq")) m = MODE_NEQ
else if (equali(mode, "max")) m = MODE_MAX
else if (equali(mode, "min")) m = MODE_MIN
else if (equali(mode, "any")) m = MODE_ANY
else continue
copy(g_szRuleCvar[g_iNumRules], charsmax(g_szRuleCvar[]), cvar)
copy(g_szRuleVal[g_iNumRules], charsmax(g_szRuleVal[]), value)
g_iRuleMode[g_iNumRules] = m
g_iNumRules++
}
fclose(fp)
log_amx("[CSB CVarCheck] loaded %d rule(s)", g_iNumRules)
}
writeDefaults(const path[])
{
new fp = fopen(path, "wt")
if (!fp)
return
fputs(fp, "; CSB CVar Checker rules: ^n")
fputs(fp, "; modes: eq (must equal), neq (must not equal), max (<=), min (>=), any (must answer)^n")
fputs(fp, "gl_fog max 1^n")
fputs(fp, "r_lightmap eq 0^n")
fputs(fp, "ex_interp max 0.1^n")
fputs(fp, "cl_lw min 1^n")
fputs(fp, "gl_max_size min 512^n")
fputs(fp, "r_drawentities eq 1^n")
fputs(fp, "cl_showevents any^n")
fclose(fp)
}
/* Read one whitespace-delimited token starting at 'start'; returns next pos or -1. */
readToken(const src[], start, out[], len)
{
new i = start
while (src[i] == ' ' || src[i] == '^t')
i++
if (!src[i])
{
out[0] = 0
return -1
}
new j = 0
while (src[i] && src[i] != ' ' && src[i] != '^t' && j < len)
out[j++] = src[i++]
out[j] = 0
return i
}