/* * 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 }