Copyright (C) 2018-2020 J_Tanzanite
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.
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 <https://www.gnu.org/licenses/>.
int sv_maxupdaterate = 0;
int ban_length_overwrite[CHEAT_MAX];
char dateformat[512] = "%Y/%m/%d %H:%M:%S";
float max_angles[3] = {89.01, 0.0, 50.01};
Handle forwardhandle = INVALID_HANDLE;
Handle forwardhandleban = INVALID_HANDLE;
Handle forwardhandleallow = INVALID_HANDLE;
bool sourcebanspp_exist = false;
bool materialadmin_exist = false;
int playerinfo_index[MAXPLAYERS + 1];
int playerinfo_tickcount[MAXPLAYERS + 1];
int playerinfo_tickcount_prev[MAXPLAYERS + 1];
int playerinfo_tickcount_diff[MAXPLAYERS + 1];
int playerinfo_buttons[MAXPLAYERS + 1][CMD_LENGTH];
int playerinfo_actions[MAXPLAYERS + 1][CMD_LENGTH];
int playerinfo_autoshoot[MAXPLAYERS + 1];
int playerinfo_jumps[MAXPLAYERS + 1];
int playerinfo_high_ping[MAXPLAYERS + 1];
int playerinfo_high_ping_warned[MAXPLAYERS + 1];
int playerinfo_query_index[MAXPLAYERS + 1];
int playerinfo_query_failed[MAXPLAYERS + 1];
int playerinfo_aimlock_sus[MAXPLAYERS + 1];
int playerinfo_aimlock[MAXPLAYERS + 1];
int playerinfo_aimbot[MAXPLAYERS + 1];
int playerinfo_bhop[MAXPLAYERS + 1];
int playerinfo_noisemaker_type[MAXPLAYERS + 1];
int playerinfo_noisemaker_ent[MAXPLAYERS + 1];
int playerinfo_noisemaker_ent_prev[MAXPLAYERS + 1];
int playerinfo_noisemaker_detection[MAXPLAYERS + 1];
float playerinfo_time_bumpercart[MAXPLAYERS + 1];
float playerinfo_time_teleported[MAXPLAYERS + 1];
float playerinfo_time_aimlock[MAXPLAYERS + 1];
float playerinfo_time_backtrack[MAXPLAYERS + 1];
float playerinfo_time_process_aimlock[MAXPLAYERS + 1];
float playerinfo_angles[MAXPLAYERS + 1][CMD_LENGTH][3];
float playerinfo_time_usercmd[MAXPLAYERS + 1][CMD_LENGTH];
float playerinfo_time_forward[MAXPLAYERS + 1][CHEAT_MAX];
bool playerinfo_banned_flags[MAXPLAYERS + 1][CHEAT_MAX];
bool playerinfo_ignore_lerp[MAXPLAYERS + 1];
"r_drawmodelstatsoverlay",
name = "[Lilac] Little Anti-Cheat",
description = "An opensource Anti-Cheat.",
public void OnPluginStart()
LoadTranslations("lilac.phrases.txt");
GetGameFolderName(gamefolder, sizeof(gamefolder));
if (StrEqual(gamefolder, "tf", false)) {
HookEvent("post_inventory_application",
event_inventoryupdate, EventHookMode_Post);
HookEvent("player_teleported",
event_teleported, EventHookMode_Post);
else if (StrEqual(gamefolder, "csgo", false)) {
if ((cvar_bhop = FindConVar("sv_autobunnyhopping")) != null) {
cvar_bhop_value = GetConVarInt(cvar_bhop);
HookConVarChange(cvar_bhop, cvar_change);
// We weren't able to get the cvar,
// disable bhop checks just in case.
PrintToServer("[Lilac] Unable to to find convar \"sv_autobunnyhopping\", bhop checks have been forcefully disabled.");
else if (StrEqual(gamefolder, "left4dead2", false)) {
// Pitch AA isn't really used much in L4D2 afaik, plus,
// like larrybrains reported, causes false positives for
// the infected team memeber smoker.
// Thanks to Larrybrains for reporting this!
max_angles = Float:{0.0, 0.0, 50.01};
else if (StrEqual(gamefolder, "left4dead", false)) {
// Same as L4D2, the smoker handles pitch differently it seems.
// Thanks to finishlast for reporting this!
max_angles = Float:{0.0, 0.0, 50.01};
else if (StrEqual(gamefolder, "dod", false)) {
PrintToServer("[Lilac] This game currently isn't supported, Little Anti-Cheat will still run, but expect some bugs and false positives/bans!");
HookEvent("player_death",
event_player_death_tf2, EventHookMode_Pre);
HookEvent("player_death",
event_player_death, EventHookMode_Pre);
HookEvent("player_spawn", event_teleported, EventHookMode_Post);
cvar[CVAR_ENABLE] = CreateConVar("lilac_enable", "1",
"Enable Little Anti-Cheat.",
FCVAR_PROTECTED, true, 0.0, true, 1.0);
cvar[CVAR_WELCOME] = CreateConVar("lilac_welcome", "0",
"Welcome connecting players saying that the server is protected.",
FCVAR_PROTECTED, true, 0.0, true, 1.0);
cvar[CVAR_SB] = CreateConVar("lilac_sourcebans", "1",
"Ban players via sourcebans++ (If it isn't installed, it will default to basebans).",
FCVAR_PROTECTED, true, 0.0, true, 1.0);
cvar[CVAR_MA] = CreateConVar("lilac_materialadmin", "1",
"Ban players via Material-Admin (Fork of Sourcebans++. If it isn't installed, will default to sourcebans++ or basebans).",
FCVAR_PROTECTED, true, 0.0, true, 1.0);
cvar[CVAR_LOG] = CreateConVar("lilac_log", "1",
FCVAR_PROTECTED, true, 0.0, true, 1.0);
cvar[CVAR_LOG_EXTRA] = CreateConVar("lilac_log_extra", "1",
"0 = Disabled.\n1 = Log extra information on player banned.\n2 = Log extra information on everything.",
FCVAR_PROTECTED, true, 0.0, true, 2.0);
cvar[CVAR_LOG_MISC] = CreateConVar("lilac_log_misc", "0",
"Log when players are kicked for misc features, like interp exploits, too high ping and on convar response failure.",
FCVAR_PROTECTED, true, 0.0, true, 1.0);
cvar[CVAR_LOG_DATE] = CreateConVar("lilac_log_date", "{year}/{month}/{day} {hour}:{minute}:{second}",
"Which date & time format to use when logging. Type: \"lilac_date_list\" for more info.",
FCVAR_PROTECTED, false, 0.0, false, 0.0);
cvar[CVAR_BAN] = CreateConVar("lilac_ban", "1",
"Enable banning of cheaters, set to 0 if you want to test Lilac before fully trusting it with bans.",
FCVAR_PROTECTED, true, 0.0, true, 1.0);
cvar[CVAR_BAN_LENGTH] = CreateConVar("lilac_ban_length", "0",
"How long bans should last in minutes (0 = forever).",
FCVAR_PROTECTED, true, 0.0, false, 0.0);
cvar[CVAR_ANGLES] = CreateConVar("lilac_angles", "1",
"Detect Angle-Cheats (Basic Anti-Aim, Legit Anti-Backstab and Duckspeed).",
FCVAR_PROTECTED, true, 0.0, true, 1.0);
cvar[CVAR_PATCH_ANGLES] = CreateConVar("lilac_angles_patch", "1",
"Patch Angle-Cheats (Basic Anti-Aim, Legit Anti-Backstab and Duckspeed).",
FCVAR_PROTECTED, true, 0.0, true, 1.0);
cvar[CVAR_CHAT] = CreateConVar("lilac_chatclear", "1",
FCVAR_PROTECTED, true, 0.0, true, 1.0);
cvar[CVAR_CONVAR] = CreateConVar("lilac_convar", "1",
"Detect basic invalid ConVars.",
FCVAR_PROTECTED, true, 0.0, true, 1.0);
cvar[CVAR_NOLERP] = CreateConVar("lilac_nolerp", "1",
FCVAR_PROTECTED, true, 0.0, true, 1.0);
cvar[CVAR_BHOP] = CreateConVar("lilac_bhop", "2",
"Detect Bhop.\n0 = Disabled.\n1 = Simplistic (Less effective, but safer).\n2 = Advanced (More aggressive, bans faster, less safe).",
FCVAR_PROTECTED, true, 0.0, true, 2.0);
cvar[CVAR_AIMBOT] = CreateConVar("lilac_aimbot", "5",
"Detect basic Aimbots.\n0 = Disabled.\n1 = Log only.\n5 or more = ban on n'th detection (Minimum possible is 5)",
FCVAR_PROTECTED, true, 0.0, false, 0.0);
cvar[CVAR_AIMBOT_AUTOSHOOT] = CreateConVar("lilac_aimbot_autoshoot", "1",
FCVAR_PROTECTED, true, 0.0, true, 1.0);
cvar[CVAR_AIMLOCK] = CreateConVar("lilac_aimlock", "10",
"Detect Aimlock.\n0 = Disabled.\n1 = Log only.\n5 or more = ban on n'th detection (Minimum possible is 5).",
FCVAR_PROTECTED, true, 0.0, false, 0.0);
cvar[CVAR_AIMLOCK_LIGHT] = CreateConVar("lilac_aimlock_light", "1",
"Only process at most 5 suspicious players for aimlock.\nDO NOT DISABLE THIS UNLESS YOUR SERVER CAN HANDLE IT!",
FCVAR_PROTECTED, true, 0.0, true, 1.0);
cvar[CVAR_ANTI_DUCK_DELAY] = CreateConVar("lilac_anti_duck_delay", "1",
"CS:GO Only, detect Anti-Duck-Delay/FastDuck.",
FCVAR_PROTECTED, true, 0.0, true, 1.0);
cvar[CVAR_NOISEMAKER_SPAM] = CreateConVar("lilac_noisemaker", "0",
"TF2 Only, detect infinite noisemaker spam. STILL IN BETA, DOES NOT BAN, ONLY LOGS! MAY HAVE SOME ISSUES!",
FCVAR_PROTECTED, true, 0.0, true, 1.0);
cvar[CVAR_BACKTRACK_PATCH] = CreateConVar("lilac_backtrack_patch", "0",
"Patch Backtrack.\n0 = Disabled (Recommended setting).\n1 = Randomized (Not recommended patch method).\n2 = Locked (Recommended patch method).",
FCVAR_PROTECTED, true, 0.0, true, 2.0);
cvar[CVAR_BACKTRACK_TOLERANCE] = CreateConVar("lilac_backtrack_tolerance", "0",
"How tolerant the backtrack patch will be of tickcount changes.\n0 = No tolerance (recommended).\n1+ = n ticks tolerant.",
FCVAR_PROTECTED, true, 0.0, true, 3.0);
cvar[CVAR_MAX_PING] = CreateConVar("lilac_max_ping", "0",
"Ban players with too high of a ping for 3 minutes.\nThis is meant to deal with fakelatency, the ban length is just to prevent instant reconnects.\n0 = no ping limit, minimum possible is 100.",
FCVAR_PROTECTED, true, 0.0, true, 1000.0);
cvar[CVAR_MAX_PING_SPEC] = CreateConVar("lilac_max_ping_spec", "0",
"Move players with a high ping to spectator and warn them after this many seconds (Minimum possible is 30).",
FCVAR_PROTECTED, true, 0.0, true, 90.0);
cvar[CVAR_MAX_LERP] = CreateConVar("lilac_max_lerp", "105",
"Kicks players attempting to exploit interpolation, any interp higher than this value = kick.\nMinimum value possible = 105 (Default interp in games = 100).\n0 or less than 105 = Disabled.",
FCVAR_PROTECTED, true, 0.0, true, 510.0); // 500 is max possible.
cvar[CVAR_LOSS_FIX] = CreateConVar("lilac_loss_fix", "1",
"Ignore some cheat detections for players who have too much packet loss (bad connection to the server).",
FCVAR_PROTECTED, true, 0.0, true, 1.0);
cvar[CVAR_AUTO_UPDATE] = CreateConVar("lilac_auto_update", "0",
"Automatically update Little Anti-Cheat.",
FCVAR_PROTECTED, true, 0.0, true, 1.0);
for (int i = 0; i < CVAR_MAX; i++) {
icvar[i] = GetConVarInt(cvar[i]);
HookConVarChange(cvar[i], cvar_change);
if ((tcvar = FindConVar("sv_maxupdaterate")) != null) {
HookConVarChange(tcvar, cvar_change);
sv_maxupdaterate = GetConVarInt(tcvar);
if ((tcvar = FindConVar("sv_cheats")) != null) {
HookConVarChange(tcvar, cvar_change);
sv_cheats = GetConVarInt(tcvar);
for (int i = 0; i < CHEAT_MAX; i++)
ban_length_overwrite[i] = -1;
// Bans for Bhop last 1 month by default.
ban_length_overwrite[CHEAT_BHOP] = 24 * 30 * 60;
// If sv_maxupdaterate is changed mid-game and then this plugin
// is loaded, then it could lead to false positives.
// Reset all stats on all players already in-game, but ignore lerp.
// Also check players already in-game for noisemaker.
for (int i = 1; i <= MaxClients; i++) {
playerinfo_ignore_lerp[i] = true;
check_inventory_for_noisemaker(i);
RegServerCmd("lilac_date_list", lilac_date_list,
"Lists date formatting options", 0);
RegServerCmd("lilac_set_ban_length", lilac_set_ban_length,
"Sets custom ban lengths for specific cheats.", 0);
RegServerCmd("lilac_ban_status", lilac_ban_status,
"Prints banning status to server console.", 0);
// Server is using the old config location, execute it.
if (FileExists("cfg/lilac_config.cfg", false, NULL_STRING)) {
AutoExecConfig(true, "lilac_config", "");
// Server either just installed Lilac, or wants to use
// the more traditional config folder.
AutoExecConfig(true, "lilac_config", "sourcemod");
forwardhandle = CreateGlobalForward("lilac_cheater_detected",
ET_Ignore, Param_Cell, Param_Cell);
forwardhandleban = CreateGlobalForward("lilac_cheater_banned",
ET_Ignore, Param_Cell, Param_Cell);
forwardhandleallow = CreateGlobalForward("lilac_allow_cheat_detection",
ET_Event, Param_Cell, Param_Cell);
CreateTimer(QUERY_TIMER, timer_query, _, TIMER_REPEAT);
CreateTimer(5.0, timer_check_ping, _, TIMER_REPEAT);
CreateTimer(5.0, timer_check_lerp, _, TIMER_REPEAT);
CreateTimer(0.5, timer_check_aimlock, _, TIMER_REPEAT);
tick_rate = RoundToNearest(1.0 / GetTickInterval());
bhop_max[BHOP_SIMPLISTIC] = 10;
bhop_max[BHOP_ADVANCED] = 5;
bhop_max[BHOP_SIMPLISTIC] = 20;
bhop_max[BHOP_ADVANCED] = 10;
lilac_log_first_time_setup();
public void OnAllPluginsLoaded()
sourcebanspp_exist = LibraryExists("sourcebans++");
materialadmin_exist = LibraryExists("materialadmin");
if (LibraryExists("updater"))
PrintToServer("[Little Anti-Cheat %s] Successfully loaded!", VERSION);
public void OnConfigsExecuted()
public Action lilac_ban_status(int args)
PrintToServer("=========[Lilac Ban Status]=========", VERSION);
PrintToServer("Checking ban plugins:");
PrintToServer("Material-Admin:");
PrintToServer("\tLoaded: %s", ((materialadmin_exist) ? "Yes" : "No"));
PrintToServer("\tNative Exists: %s", ((NATIVE_EXISTS("MABanPlayer")) ? "Yes" : "No"));
PrintToServer("\tConVar: lilac_materialadmin = %d", icvar[CVAR_MA]);
PrintToServer("\tWARNING: Material-Admin was NOT included when compiled, banning through MA won't work!");
PrintToServer("\tПредупреждение: Material-Admin НЕ БЫЛ включен при компиляции, баны через MA не будут работать!");
ban_type = ((icvar[CVAR_MA] && NATIVE_EXISTS("MABanPlayer")) ? 2 : 0);
PrintToServer("Sourcebans++:");
PrintToServer("\tLoaded: %s", ((sourcebanspp_exist) ? "Yes" : "No"));
PrintToServer("\tNative Exists: %s", ((NATIVE_EXISTS("SBPP_BanPlayer")) ? "Yes" : "No"));
PrintToServer("\tConVar: lilac_sourcebans = %d", icvar[CVAR_SB]);
PrintToServer("\tWARNING: Sourcebans++ was NOT included when compiled, banning through SB++ won't work!");
ban_type = (icvar[CVAR_SB] && NATIVE_EXISTS("SBPP_BanPlayer"));
case 0: { strcopy(tmp, sizeof(tmp), "BaseBans"); }
case 1: { strcopy(tmp, sizeof(tmp), "Sourcebans++"); }
case 2: { strcopy(tmp, sizeof(tmp), "Material-Admin"); }
PrintToServer("\nBanning will go though %s.\n", tmp);
public Action lilac_set_ban_length(int args)
char feature[32], length[32];
PrintToServer("Error: Too few arguments.\n\nUsage:\t\tlilac_set_ban_length <cheat> <minutes>");
PrintToServer("Example:\tlilac_set_ban_length bhop 15\n\nSets bhop ban to 15 minutes.");
PrintToServer("If ban length is -1, then the length will be ConVar lilac_ban_length\n");
PrintToServer("Possible cheat arguments:");
PrintToServer("\tlilac_set_ban_length angles <minutes>");
PrintToServer("\tlilac_set_ban_length chatclear <minutes>");
PrintToServer("\tlilac_set_ban_length convar <minutes>");
PrintToServer("\tlilac_set_ban_length nolerp <minutes>");
PrintToServer("\tlilac_set_ban_length bhop <minutes>");
PrintToServer("\tlilac_set_ban_length aimbot <minutes>");
PrintToServer("\tlilac_set_ban_length aimlock <minutes>");
PrintToServer("\tlilac_set_ban_length antiduckdelay <minutes>");
PrintToServer("\tlilac_set_ban_length noisemaker <minutes>\n");
GetCmdArg(1, feature, sizeof(feature));
if (StrEqual(feature, "angles", false) || StrEqual(feature, "angle", false)) {
else if (StrEqual(feature, "chat", false) || StrEqual(feature, "chatclear", false)) {
else if (StrEqual(feature, "convar", false) || StrEqual(feature, "cvar", false)) {
else if (StrEqual(feature, "nolerp", false)) {
else if (StrEqual(feature, "bhop", false) || StrEqual(feature, "bunnyhop", false)) {
else if (StrEqual(feature, "aimbot", false) || StrEqual(feature, "aim", false)) {
else if (StrEqual(feature, "aimlock", false) || StrEqual(feature, "lock", false)) {
// ( @~@) Bruh.... This is... B R U H
else if (StrEqual(feature, "duck", false) || StrEqual(feature, "crouch", false)
|| StrEqual(feature, "antiduck", false) || StrEqual(feature, "antiduckdelay", false)
|| StrEqual(feature, "fastduck", false)) {
index = CHEAT_ANTI_DUCK_DELAY;
else if (StrEqual(feature, "noisemaker", false) || StrEqual(feature, "noise", false)) {
index = CHEAT_NOISEMAKER_SPAM;
PrintToServer("Error: Unknown cheat feature \"%s\"", feature);
GetCmdArg(2, length, sizeof(length));
time = StringToInt(length, 10);
ban_length_overwrite[index] = time;
public Action lilac_date_list(int args)
PrintToServer("=======[Lilac Date Formatting]=======");
PrintToServer("Manual formatting:");
PrintToServer("\t{raw} = Skips the special formatting listed here");
PrintToServer("\t and lets you insert your own formatting");
PrintToServer("\t (see: http://www.cplusplus.com/reference/ctime/strftime/).");
PrintToServer("Example:\n\t{raw}%%Y/%%m/%%d %%H:%%M:%%S");
PrintToServer("\t{year} = Numerical year (2020).");
PrintToServer("\t{month} = Numerical month (12).");
PrintToServer("\t{day} = Numerical day (28).");
PrintToServer("\t{hour} = 24 hour format.");
PrintToServer("\t{hours} = 24 hour format.");
PrintToServer("\t{24hour} = 24 hour format.");
PrintToServer("\t{24hours} = 24 hour format.");
PrintToServer("\t{12hour} = 12 hour format.");
PrintToServer("\t{12hours} = 12 hour format.");
PrintToServer("\t{pm} = Insert AM/PM.");
PrintToServer("\t{am} = Insert AM/PM.");
PrintToServer("\t{minute} = Minute.");
PrintToServer("\t{minutes} = Minute.");
PrintToServer("\t{second} = Second.");
PrintToServer("\t{seconds} = Second.");
PrintToServer("Using flags example: {year}/{month}/{day} {hour}:{minute}:{second}");
public APLRes AskPluginLoad2(Handle hMyself, bool bLate, char[] sError, int err_max)
// Been told this isn't needed, but just in case.
MarkNativeAsOptional("SBPP_BanPlayer");
MarkNativeAsOptional("MABanPlayer");
MarkNativeAsOptional("Updater_AddPlugin");
MarkNativeAsOptional("Updater_RemovePlugin");
public void OnLibraryAdded(const char []name)
if (StrEqual(name, "sourcebans++"))
sourcebanspp_exist = true;
else if (StrEqual(name, "materialadmin"))
materialadmin_exist = true;
else if (StrEqual(name, "updater"))
public void OnLibraryRemoved(const char []name)
if (StrEqual(name, "sourcebans++"))
sourcebanspp_exist = false;
else if (StrEqual(name, "materialadmin"))
materialadmin_exist = false;
public void cvar_change(ConVar convar, const char[] oldValue,
// Thanks to MAGNAT2645 for informing me I could do this!
if (view_as<Handle>(convar) == cvar[CVAR_ENABLE]) {
icvar[CVAR_ENABLE] = StringToInt(newValue, 10);
else if (view_as<Handle>(convar) == cvar[CVAR_WELCOME]) {
icvar[CVAR_WELCOME] = StringToInt(newValue, 10);
else if (view_as<Handle>(convar) == cvar[CVAR_SB]) {
icvar[CVAR_SB] = StringToInt(newValue, 10);
else if (view_as<Handle>(convar) == cvar[CVAR_MA]) {
icvar[CVAR_MA] = StringToInt(newValue, 10);
else if (view_as<Handle>(convar) == cvar[CVAR_LOG]) {
icvar[CVAR_LOG] = StringToInt(newValue, 10);
else if (view_as<Handle>(convar) == cvar[CVAR_LOG_EXTRA]) {
icvar[CVAR_LOG_EXTRA] = StringToInt(newValue, 10);
else if (view_as<Handle>(convar) == cvar[CVAR_LOG_MISC]) {
icvar[CVAR_LOG_MISC] = StringToInt(newValue, 10);
else if (view_as<Handle>(convar) == cvar[CVAR_LOG_DATE]) {
lilac_setup_date_format(newValue);
FormatTime(testdate, sizeof(testdate), dateformat, GetTime());
PrintToServer("Date Format Preview: %s", testdate);
else if (view_as<Handle>(convar) == cvar[CVAR_BAN]) {
icvar[CVAR_BAN] = StringToInt(newValue, 10);
PrintToServer("[Little Anti-Cheat %s] WARNING: 'lilac_ban' has been set to 0, banning of cheaters has been disabled.", VERSION);
else if (view_as<Handle>(convar) == cvar[CVAR_BAN_LENGTH]) {
icvar[CVAR_BAN_LENGTH] = StringToInt(newValue, 10);
else if (view_as<Handle>(convar) == cvar[CVAR_ANGLES]) {
icvar[CVAR_ANGLES] = StringToInt(newValue, 10);
else if (view_as<Handle>(convar) == cvar[CVAR_PATCH_ANGLES]) {
icvar[CVAR_PATCH_ANGLES] = StringToInt(newValue, 10);
else if (view_as<Handle>(convar) == cvar[CVAR_CHAT]) {
icvar[CVAR_CHAT] = StringToInt(newValue, 10);
else if (view_as<Handle>(convar) == cvar[CVAR_CONVAR]) {
icvar[CVAR_CONVAR] = StringToInt(newValue, 10);
else if (view_as<Handle>(convar) == cvar[CVAR_NOLERP]) {
icvar[CVAR_NOLERP] = StringToInt(newValue, 10);
else if (view_as<Handle>(convar) == cvar[CVAR_BHOP]) {
icvar[CVAR_BHOP] = StringToInt(newValue, 10);
else if (view_as<Handle>(convar) == cvar[CVAR_AIMBOT]) {
icvar[CVAR_AIMBOT] = StringToInt(newValue, 10);
if (icvar[CVAR_AIMBOT] > 1 &&
icvar[CVAR_AIMBOT] < AIMBOT_BAN_MIN)
else if (view_as<Handle>(convar) == cvar[CVAR_AIMBOT_AUTOSHOOT]) {
icvar[CVAR_AIMBOT_AUTOSHOOT] = StringToInt(newValue, 10);
else if (view_as<Handle>(convar) == cvar[CVAR_AIMLOCK]) {
icvar[CVAR_AIMLOCK] = StringToInt(newValue, 10);
if (icvar[CVAR_AIMLOCK] > 1
&& icvar[CVAR_AIMLOCK] < AIMLOCK_BAN_MIN)
else if (view_as<Handle>(convar) == cvar[CVAR_AIMLOCK_LIGHT]) {
icvar[CVAR_AIMLOCK_LIGHT] = StringToInt(newValue, 10);
else if (view_as<Handle>(convar) == cvar[CVAR_ANTI_DUCK_DELAY]) {
icvar[CVAR_ANTI_DUCK_DELAY] = StringToInt(newValue, 10);
else if (view_as<Handle>(convar) == cvar[CVAR_NOISEMAKER_SPAM]) {
icvar[CVAR_NOISEMAKER_SPAM] = StringToInt(newValue, 10);
else if (view_as<Handle>(convar) == cvar[CVAR_BACKTRACK_PATCH]) {
icvar[CVAR_BACKTRACK_PATCH] = StringToInt(newValue, 10);
if (icvar[CVAR_BACKTRACK_PATCH] == 1)
PrintToServer("[Little Anti-Cheat %s] WARNING: Patch method 1 isn't recommended, use patch method 2 instead.", VERSION);
else if (view_as<Handle>(convar) == cvar[CVAR_BACKTRACK_TOLERANCE]) {
icvar[CVAR_BACKTRACK_TOLERANCE] = StringToInt(newValue, 10);
if (icvar[CVAR_BACKTRACK_TOLERANCE] > 2)
PrintToServer("[Little Anti-Cheat %s] WARNING: It is not recommeded to set backtrack tolerance above 2, only do this if you understand what this means.", VERSION);
else if (view_as<Handle>(convar) == cvar[CVAR_MAX_PING]) {
icvar[CVAR_MAX_PING] = StringToInt(newValue, 10);
else if (view_as<Handle>(convar) == cvar[CVAR_MAX_PING_SPEC]) {
icvar[CVAR_MAX_PING_SPEC] = StringToInt(newValue, 10);
if (icvar[CVAR_MAX_PING_SPEC] < 30)
icvar[CVAR_MAX_PING_SPEC] = 0;
else if (view_as<Handle>(convar) == cvar[CVAR_MAX_LERP]) {
icvar[CVAR_MAX_LERP] = StringToInt(newValue, 10);
else if (view_as<Handle>(convar) == cvar[CVAR_LOSS_FIX]) {
icvar[CVAR_LOSS_FIX] = StringToInt(newValue, 10);
else if (view_as<Handle>(convar) == cvar[CVAR_AUTO_UPDATE]) {
icvar[CVAR_AUTO_UPDATE] = StringToInt(newValue, 10);
GetConVarName(convar, cvarname, sizeof(cvarname));
if (StrEqual(cvarname, "sv_autobunnyhopping", false)) {
cvar_bhop_value = StringToInt(newValue, 10);
else if (StrEqual(cvarname, "sv_maxupdaterate", false)) {
sv_maxupdaterate = StringToInt(newValue);
// Changing this convar mid-game can cause false positives.
// Ignore players already in-game.
for (int i = 1; i <= MaxClients; i++)
playerinfo_ignore_lerp[i] = true;
else if (StrEqual(cvarname, "sv_cheats", false)) {
sv_cheats = StringToInt(newValue);
// Delay convar checks for 30 seconds.
time_sv_cheats = GetTime() + QUERY_TIMEOUT;
if (icvar[CVAR_AUTO_UPDATE]) {
if (!NATIVE_EXISTS("Updater_AddPlugin")) {
PrintToServer("Error: Native Updater_AddPlugin() not found! Check if updater plugin is installed.");
Updater_AddPlugin(UPDATE_URL);
if (!NATIVE_EXISTS("Updater_RemovePlugin")) {
PrintToServer("Error: Native Updater_RemovePlugin() not found! Check if updater plugin is installed.");
PrintToServer("Error: Auto updater wasn't included when compiled, auto updating won't work!");
public void OnClientPutInServer(int client)
lilac_reset_client(client);
CreateTimer(20.0, timer_welcome, GetClientUserId(client));
void lilac_reset_client(int client)
playerinfo_ignore_lerp[client] = false;
playerinfo_index[client] = 0;
playerinfo_tickcount[client] = 0;
playerinfo_tickcount_prev[client] = 0;
playerinfo_tickcount_diff[client] = 0;
playerinfo_autoshoot[client] = 0;
playerinfo_jumps[client] = 0;
playerinfo_high_ping[client] = 0;
playerinfo_high_ping_warned[client] = 0;
playerinfo_query_index[client] = 0;
playerinfo_query_failed[client] = 0;
playerinfo_aimlock_sus[client] = 0;
playerinfo_aimlock[client] = 0;
playerinfo_aimbot[client] = 0;
playerinfo_bhop[client] = 0;
playerinfo_noisemaker_type[client] = NOISEMAKER_TYPE_NONE;
playerinfo_noisemaker_ent[client] = 0;
playerinfo_noisemaker_ent_prev[client] = 0;
playerinfo_noisemaker_detection[client] = 0;
playerinfo_time_bumpercart[client] = 0.0;
playerinfo_time_teleported[client] = 0.0;
playerinfo_time_aimlock[client] = 0.0;
playerinfo_time_backtrack[client] = 0.0;
playerinfo_time_process_aimlock[client] = 0.0;
for (int i = 0; i < CHEAT_MAX; i++) {
playerinfo_time_forward[client][i] = 0.0;
playerinfo_banned_flags[client][i] = false;
for (int i = 0; i < CMD_LENGTH; i++) {
playerinfo_buttons[client][i] = 0;
playerinfo_actions[client][i] = 0;
playerinfo_time_usercmd[client][i] = 0.0;
set_player_log_angles(client, Float:{0.0, 0.0, 0.0}, i);
public void TF2_OnConditionAdded(int client, TFCond condition)
if (condition == TFCond_HalloweenKart)
playerinfo_time_bumpercart[client] = GetGameTime();
public void TF2_OnConditionRemoved(int client, TFCond condition)
if (condition == TFCond_Taunting)
playerinfo_time_teleported[client] = GetGameTime();
public Action event_inventoryupdate(Event event, const char[] name, bool dontBroadcast)
client = GetClientOfUserId(GetEventInt(event, "userid", 0));
check_inventory_for_noisemaker(client);
void check_inventory_for_noisemaker(int client)
if (!is_player_valid(client))
playerinfo_noisemaker_type[client] = NOISEMAKER_TYPE_NONE;
playerinfo_noisemaker_ent_prev[client] = playerinfo_noisemaker_ent[client];
playerinfo_noisemaker_ent[client] = 0;
for (int i = MaxClients + 1; i < GetEntityCount(); i++) {
if (GetEntPropEnt(i, Prop_Data, "m_hOwnerEntity") != client)
GetEntityClassname(i, classname, sizeof(classname));
if (!StrEqual(classname, "tf_wearable", false))
type = get_entity_noisemaker_type(GetEntProp(i, Prop_Send, "m_iItemDefinitionIndex"));
playerinfo_noisemaker_type[client] = type;
playerinfo_noisemaker_ent[client] = i;
public Action event_teleported(Event event, const char[] name,
int client = GetClientOfUserId(GetEventInt(event, "userid", -1));
if (is_player_valid(client))
playerinfo_time_teleported[client] = GetGameTime();
public Action event_player_death(Event event, const char[] name,
int userid = GetEventInt(event, "attacker", -1);
int client = GetClientOfUserId(userid);
int victim = GetClientOfUserId(GetEventInt(event, "userid", -1));
event_death_shared(userid, client, victim, false);
// Todo: Ehh, needs some work, not pretty.
public Action event_player_death_tf2(Event event, const char[] name,
int userid = GetEventInt(event, "attacker", -1);
int client = GetClientOfUserId(userid);
int victim = GetClientOfUserId(GetEventInt(event, "userid", -1));
int killtype = GetEventInt(event, "customkill", 0);
GetEventString(event, "weapon_logclassname", wep, sizeof(wep), "");
// Ignore sentries in TF2.
if (!strncmp(wep, "obj_", 4, false))
// Killtype 3 = flamethrower.
event_death_shared(userid, client, victim,
((killtype == 3) ? true : false));
void event_death_shared(int userid, int client, int victim, bool skip_delta)
float killpos[3], deathpos[3];
if (!is_player_valid(client)
|| !is_player_valid(victim)
|| playerinfo_banned_flags[client][CHEAT_AIMBOT]
|| GetClientTime(client) < 10.1)
if (icvar[CVAR_AIMLOCK_LIGHT])
lilac_aimlock_light_test(client);
GetClientEyePosition(client, killpos);
GetClientEyePosition(victim, deathpos);
// Killer and victim are too close to each other,
if (GetVectorDistance(killpos, deathpos) < 350.0 || skip_delta)
CreateDataTimer(0.5, timer_check_aimbot, pack);
pack.WriteCell(skip_snap);
// Fallback to this tick if the shot isn't found.
pack.WriteCell(playerinfo_index[client]);
pack.WriteFloat(killpos[0]);
pack.WriteFloat(killpos[1]);
pack.WriteFloat(killpos[2]);
pack.WriteFloat(deathpos[0]);
pack.WriteFloat(deathpos[1]);
pack.WriteFloat(deathpos[2]);
public Action timer_welcome(Handle timer, int userid)
int client = GetClientOfUserId(userid);
if (is_player_valid(client) && icvar[CVAR_WELCOME]
&& icvar[CVAR_ENABLE] && icvar[CVAR_BAN])
PrintToChat(client, "[Lilac] %T", "welcome_msg", client, VERSION);
public Action timer_query(Handle timer)
if (!icvar[CVAR_ENABLE] || !icvar[CVAR_CONVAR])
// sv_cheats recently changed or is set to 1, abort.
if (GetTime() < time_sv_cheats || sv_cheats)
for (int i = 1; i <= MaxClients; i++) {
if (!is_player_valid(i) || IsFakeClient(i))
// Player recently joined, wait before querying.
if (GetClientTime(i) < 60.0)
// Don't query already banned players.
if (playerinfo_banned_flags[i][CHEAT_CONVAR])
// Only increments query index if the player
// has responded to the last one.
if (!playerinfo_query_failed[i]) {
if (++playerinfo_query_index[i] >= 11)
playerinfo_query_index[i] = 0;
QueryClientConVar(i, query_list[playerinfo_query_index[i]], query_reply, 0);
if (++playerinfo_query_failed[i] > QUERY_MAX_FAILURES) {
if (icvar[CVAR_LOG_MISC]) {
lilac_log_setup_client(i);
Format(line, sizeof(line),
"%s was kicked for failing to respond to %d queries in %.0f seconds.",
line, QUERY_MAX_FAILURES,
QUERY_TIMER * QUERY_MAX_FAILURES);
if (icvar[CVAR_LOG_EXTRA] == 2)
KickClient(i, "[Lilac] %T", "kick_query_failure", i);
public void query_reply(QueryCookie cookie, int client,
ConVarQueryResult result, const char[] cvarName,
const char[] cvarValue, any value)
// Player NEEDS to answer the query.
if (result != ConVarQuery_Okay)
// Client did respond to the query request, move on to the next convar.
playerinfo_query_failed[client] = 0;
// Any response the server may recieve may also be faulty, ignore.
if (GetTime() < time_sv_cheats || sv_cheats)
if (playerinfo_banned_flags[client][CHEAT_CONVAR])
int val = StringToInt(cvarValue);
// Check for invalid convar responses.
// Other than drawothermodels, a value of non-zero is invalid.
if (StrEqual("r_drawothermodels", cvarName, false) && val == 1)
if (lilac_forward_allow_cheat_detection(client, CHEAT_CONVAR) == false)
lilac_forward_client_cheat(client, CHEAT_CONVAR);
lilac_log_setup_client(client);
Format(line, sizeof(line),
"%s was detected and banned for an invalid ConVar (%s %s).",
line, cvarName, cvarValue);
if (icvar[CVAR_LOG_EXTRA])
playerinfo_banned_flags[client][CHEAT_CONVAR] = true;
lilac_ban_client(client, CHEAT_CONVAR);
public Action timer_check_lerp(Handle timer)
if (sv_maxupdaterate > 0)
min = 1.0 / float(sv_maxupdaterate);
for (int i = 1; i <= MaxClients; i++) {
if (!is_player_valid(i) || IsFakeClient(i))
float lerp = GetEntPropFloat(i, Prop_Data, "m_fLerpTime");
if (lerp * 1000.0 > float(icvar[CVAR_MAX_LERP]) && icvar[CVAR_MAX_LERP] >= 105) {
if (icvar[CVAR_LOG_MISC]) {
lilac_log_setup_client(i);
Format(line, sizeof(line),
"%s was kicked for exploiting interpolation (%.3fms / %dms max).",
line, lerp * 1000.0, icvar[CVAR_MAX_LERP]);
if (icvar[CVAR_LOG_EXTRA] == 2)
KickClient(i, "[Lilac] %T", "kick_interp_exploit", i,
lerp * 1000.0, icvar[CVAR_MAX_LERP], float(icvar[CVAR_MAX_LERP]) / 999.9);
|| playerinfo_ignore_lerp[i]
|| playerinfo_banned_flags[i][CHEAT_NOLERP]
|| min < 0.005) // Minvalue invalid or too low.
if (lerp > min * 0.95 /* buffer */)
if (lilac_forward_allow_cheat_detection(i, CHEAT_NOLERP) == false)
playerinfo_banned_flags[i][CHEAT_NOLERP] = true;
lilac_forward_client_cheat(i, CHEAT_NOLERP);
lilac_log_setup_client(i);
Format(line, sizeof(line),
"%s was detected and banned for NoLerp (%fms).",
if (icvar[CVAR_LOG_EXTRA])
lilac_ban_client(i, CHEAT_NOLERP);
public Action timer_check_ping(Handle timer)
static bool toggle = true;
if (!icvar[CVAR_ENABLE] || icvar[CVAR_MAX_PING] < 100)
for (int i = 1; i <= MaxClients; i++) {
if (!is_player_valid(i) || IsFakeClient(i))
// Player recently joined, don't check ping yet.
if (GetClientTime(i) < 120.0)
ping = GetClientAvgLatency(i, NetFlow_Outgoing) * 1000.0;
if (ping < float(icvar[CVAR_MAX_PING])) {
if (toggle && playerinfo_high_ping[i] > 0)
playerinfo_high_ping[i]--;
if (playerinfo_high_ping[i] < playerinfo_high_ping_warned[i] - 2
&& playerinfo_high_ping_warned[i] > 0) {
playerinfo_high_ping_warned[i] = 0;
PrintToChat(i, "[Lilac] Your ping appears to be fine again, it is safe to rejoin a team and play.");
if (++playerinfo_high_ping[i] >= icvar[CVAR_MAX_PING_SPEC] / 5 && icvar[CVAR_MAX_PING_SPEC] >= 30) {
ChangeClientTeam(i, 1); // Move this player to spectators.
playerinfo_high_ping_warned[i] = playerinfo_high_ping[i];
PrintToChat(i, "[Lilac] WARNING: You will be kicked in %d seconds if your ping stays too high! (%.0f / %d max)",
100 - (playerinfo_high_ping[i] * 5),
ping, icvar[CVAR_MAX_PING]);
// Player has a higher ping than maximum for 100 seconds.
if (playerinfo_high_ping[i] < 20)
if (icvar[CVAR_LOG_MISC]) {
lilac_log_setup_client(i);
Format(line, sizeof(line),
"%s was kicked for having too high ping (%.3fms / %dms max).",
line, ping, icvar[CVAR_MAX_PING]);
if (icvar[CVAR_LOG_EXTRA] == 2)
Format(reason, sizeof(reason),
"[Lilac] %T", "tban_ping_high", i,
ping, icvar[CVAR_MAX_PING]);
// Ban the client for three minutes to avoid instant reconnects.
BanClient(i, 3, BANFLAG_AUTHID, reason, reason, "lilac", 0);
public Action timer_check_aimlock(Handle timer)
float ang[3], lang[3], ideal[3], pos[3], pos2[3];
bool skip_report[MAXPLAYERS + 1]; // Skip reporting this player.
bool report[MAXPLAYERS + 1]; // report this player.
bool process; // Keep processing the player.
int players_processed = 0;
if (!icvar[CVAR_ENABLE] || !icvar[CVAR_AIMLOCK])
for (int i = 1; i <= MaxClients; i++) {
// Don't process more than 5 players!
if (players_processed >= 5 && icvar[CVAR_AIMLOCK_LIGHT] == 1)
if (!is_player_valid(i) || IsFakeClient(i))
// Player must be alive and on a valid team.
if (!IsPlayerAlive(i) || GetClientTeam(i) < 2)
// Player recently teleported or taunted, ignore angle snaps.
if (GetGameTime() - playerinfo_time_teleported[i] < 2.0)
// Player has too much packet loss.
// Player already banned for aimlock, don't need to check for it.
if (playerinfo_banned_flags[i][CHEAT_AIMLOCK])
// If lightmode is enabled, the player must be in the process que.
if (icvar[CVAR_AIMLOCK_LIGHT] == 1 && lilac_is_player_in_aimlock_que(i) == false)
GetClientEyePosition(i, pos);
for (int k = 1; k <= MaxClients && process; k++) {
if (!is_player_valid(k) || k == i)
if (GetClientTeam(k) == GetClientTeam(i))
// Player2 needs to be alive and on a valid team as well.
if (!IsPlayerAlive(k) || GetClientTeam(k) < 2)
GetClientEyePosition(k, pos2);
// Players are too close, never report aimlock.
if (GetVectorDistance(pos, pos2) < 300.0) {
// Player target teleported, skip testing.
if (GetGameTime() - playerinfo_time_teleported[k] < 2.0)
aim_at_point(pos, pos2, ideal);
int ind = playerinfo_index[i];
for (int l = 0; l < time_to_ticks(0.5 + 0.1); l++) {
// Only process ticks that happened 0.5 seconds ago... Plus lock_time.
if (GetGameTime() - playerinfo_time_usercmd[i][ind] < 0.5 + 0.1) {
get_player_log_angles(i, ind, false, ang);
laimdist = angle_delta(ang, ideal);
if (aimdist < laimdist * 0.1
&& angle_delta(ang, lang) > 20.0
&& lock > time_to_ticks(0.1)) {
for (int i = 1; i <= MaxClients; i++) {
if (skip_report[i] || !report[i])
lilac_detected_aimlock(i);
public Action timer_check_aimbot(Handle timer, DataPack pack)
float ideal[3], ang[3], lang[3];
float killpos[3], deathpos[3];
bool skip_autoshoot = false;
bool skip_repeat = false;
client = GetClientOfUserId(pack.ReadCell());
skip_snap = pack.ReadCell();
fallback = pack.ReadCell();
killpos[0] = pack.ReadFloat();
killpos[1] = pack.ReadFloat();
killpos[2] = pack.ReadFloat();
deathpos[0] = pack.ReadFloat();
deathpos[1] = pack.ReadFloat();
deathpos[2] = pack.ReadFloat();
// Killer may have left the game, cancel.
if (!is_player_valid(client))
// Locate when the shot was fired.
ind = playerinfo_index[client];
// 0.5 (datapacktimer delay) + 0.5 (snap test) + 0.1 (buffer).
// We are looking this far back in case of a projectile aimbot shot,
// as the death event happens way later after the shot.
for (int i = 0; i < CMD_LENGTH - time_to_ticks(0.5 + 0.5 + 0.1); i++) {
// The shot needs to have happened at least 0.3 seconds ago.
if (GetGameTime() - playerinfo_time_usercmd[client][ind] < 0.3)
if ((playerinfo_actions[client][ind] & ACTION_SHOT)) {
// Shot not found, use fallback.
// If the latest index is the same as the fallback, then no
// more usercmds have been processed since the death event.
// These detections are thus unstable and will be ignored
// (They require at least one tick after the shot to work).
if (playerinfo_index[client] == fallback) {
// Don't detect the same shot twice.
playerinfo_actions[client][shotindex] = 0;
// Forgot to add this in the past, oops...
// Skip repeat detections if players are too close to each other.
// Player taunted within 0.5 seconds of taking a shot leading to a kill.
// Ignore snap detections.
if (-0.1 < playerinfo_time_usercmd[client][shotindex] - playerinfo_time_teleported[client] < 0.5 + 0.1)
// Aimsnap and total delta test.
if (skip_snap == false) {
aim_at_point(killpos, deathpos, ideal);
for (int i = 0; i < time_to_ticks(0.5); i++) {
// We're looking back further than 0.5 seconds prior to the shot, abort.
if (playerinfo_time_usercmd[client][shotindex] - playerinfo_time_usercmd[client][ind] > 0.5)
laimdist = angle_delta(playerinfo_angles[client][ind], ideal);
get_player_log_angles(client, ind, false, ang);
tdelta = angle_delta(lang, ang);
if (aimdist < laimdist * 0.2 && tdelta > 10.0)
detected |= AIMBOT_FLAG_SNAP;
if (aimdist < laimdist * 0.1 && tdelta > 5.0)
detected |= AIMBOT_FLAG_SNAP2;
// Packetloss is too high, skip all detections but total_delta.
if (skip_due_to_loss(client)) {
if (skip_repeat == false) {
get_player_log_angles(client, shotindex - 1, false, ang);
get_player_log_angles(client, shotindex + 1, false, lang);
tdelta = angle_delta(ang, lang);
get_player_log_angles(client, shotindex, false, lang);
if (tdelta < 10.0 && angle_delta(ang, lang) > 0.5
&& angle_delta(ang, lang) > tdelta * 5.0)
detected |= AIMBOT_FLAG_REPEAT;
if (skip_autoshoot == false && icvar[CVAR_AIMBOT_AUTOSHOOT]) {
for (int i = 0; i < 3; i++) {
else if (ind >= CMD_LENGTH)
if ((playerinfo_buttons[client][ind] & IN_ATTACK))
// Players must get two of them in a row leading to a kill
// or something else must have been detected to get this flag.
if (detected || ++playerinfo_autoshoot[client] > 1)
detected |= AIMBOT_FLAG_AUTOSHOOT;
playerinfo_autoshoot[client] = 0;
if (detected || total_delta > AIMBOT_MAX_TOTAL_DELTA)
lilac_detected_aimbot(client, delta, total_delta, detected);
public Action OnClientCommandKeyValues(int client, KeyValues kv)
KvGetSectionName(kv, command, sizeof(command));
if (!icvar[CVAR_ENABLE] || !icvar[CVAR_NOISEMAKER_SPAM])
if (playerinfo_noisemaker_type[client] != NOISEMAKER_TYPE_LIMITED)
if (playerinfo_noisemaker_ent_prev[client] != playerinfo_noisemaker_ent[client]) {
playerinfo_noisemaker_ent_prev[client] = playerinfo_noisemaker_ent[client];
playerinfo_noisemaker_detection[client] = 0;
if (StrEqual(command, "+use_action_slot_item_server", false)
|| StrEqual(command, "-use_action_slot_item_server", false)) {
// Since this reacts to both + and -, and the maximum is 25 uses per noisemaker,
// detect the double of that + a buffer of 10.
if (++playerinfo_noisemaker_detection[client] > 60)
lilac_detected_noisemaker(client);
public Action OnPlayerRunCmd(int client, int& buttons, int& impulse,
float vel[3], float angles[3], int& weapon,
int& subtype, int& cmdnum, int& tickcount,
static int lbuttons[MAXPLAYERS + 1];
if (!is_player_valid(client) || IsFakeClient(client))
if (++playerinfo_index[client] >= CMD_LENGTH)
playerinfo_index[client] = 0;
// Store when the tick was processed.
playerinfo_time_usercmd[client][playerinfo_index[client]] = GetGameTime();
set_player_log_angles(client, angles, playerinfo_index[client]);
playerinfo_buttons[client][playerinfo_index[client]] = buttons;
playerinfo_actions[client][playerinfo_index[client]] = 0;
if ((buttons & IN_ATTACK) && bullettime_can_shoot(client))
playerinfo_actions[client][playerinfo_index[client]] |= ACTION_SHOT;
// We need to store information even if the plugin is disabled,
// incase it gets turned on again mid-game.
if (!icvar[CVAR_ENABLE]) {
lbuttons[client] = buttons;
playerinfo_tickcount_prev[client] = playerinfo_tickcount[client];
playerinfo_tickcount[client] = tickcount;
// Detect Anti-Duck-Delay
if (ggame == GAME_CSGO && icvar[CVAR_ANTI_DUCK_DELAY] && (buttons & IN_BULLRUSH))
lilac_detected_anti_duck_delay(client);
// Backtrack setup, even if patch is disabled, these values
// need to be stored incase the backtrack patch gets
playerinfo_tickcount_prev[client] = playerinfo_tickcount[client];
playerinfo_tickcount[client] = tickcount;
if (icvar[CVAR_BACKTRACK_PATCH]) {
if (lilac_valid_tickcount(client) == false
&& lilac_is_player_in_backtrack_timeout(client) == false)
lilac_set_client_in_backtrack_timeout(client);
// Patch backtracking by manipulating the tickcount.
if (lilac_is_player_in_backtrack_timeout(client)) {
switch (icvar[CVAR_BACKTRACK_PATCH]) {
case 1: { tickcount = lilac_random_tickcount(client); }
case 2: { tickcount = lilac_lock_tickcount(client); }
// Hotfix for extremely niece and rare TF2 bug...
if (TF2_IsPlayerInCondition(client, TFCond_HalloweenKart))
playerinfo_time_bumpercart[client] = GetGameTime();
// Detect angles that are out of bounds.
// Ignore players who recently teleported.
if (icvar[CVAR_ANGLES] && IsPlayerAlive(client)
&& GetGameTime() > playerinfo_time_teleported[client] + 5.0
&& GetGameTime() > playerinfo_time_bumpercart[client] + 1.0) {
if ((FloatAbs(angles[0]) > max_angles[0] && max_angles[0])
|| (FloatAbs(angles[2]) > max_angles[2] && max_angles[2]))
lilac_detected_angles(client);
// Patch out of bounds angles.
if (icvar[CVAR_PATCH_ANGLES]) {
if (max_angles[0] != 0.0) {
if (angles[0] > max_angles[0])
angles[0] = max_angles[0];
else if (angles[0] < (max_angles[0] * -1.0))
angles[0] = (max_angles[0] * -1.0);
if (icvar[CVAR_BHOP] && !cvar_bhop_value) {
playerinfo_jumps[client]++;
int flags = GetEntityFlags(client);
if ((buttons & IN_JUMP) && !(lbuttons[client] & IN_JUMP)) {
if ((flags & FL_ONGROUND)) {
lilac_detected_bhop(client);
playerinfo_bhop[client]++;
else if ((flags & FL_ONGROUND)) {
playerinfo_bhop[client] = 0;
playerinfo_jumps[client] = 0;
lbuttons[client] = buttons;
int lilac_lock_tickcount(int client)
ping = RoundToNearest(GetClientAvgLatency(client, NetFlow_Outgoing) / GetTickInterval());
tick = playerinfo_tickcount_diff[client] + (GetGameTickCount() - ping);
// Never return higher than server tick count.
// Other than that, lock the tickcount to the player's
// previous value for the durration of the patch.
// This patch method shouldn't affect legit laggy players as much.
return ((tick > GetGameTickCount()) ? GetGameTickCount() : tick);
int lilac_random_tickcount(int client)
int tick, ping, forwardtrack;
// Latency/Ping in ticks.
ping = RoundToNearest(GetClientAvgLatency(client, NetFlow_Outgoing) / GetTickInterval());
// Forwardtracking is maximum 200ms.
if (forwardtrack > time_to_ticks(0.2))
forwardtrack = time_to_ticks(0.2);
// Randomize tickcount to be what it should be (server tickcount - ping)
// - a random value between -200ms and forwardtracking (max 200ms).
tick = GetGameTickCount() - ping + GetRandomInt(0, time_to_ticks(0.2) + forwardtrack) - time_to_ticks(0.2);
// Tickcount cannot be larger than server tickcount.
if (tick > GetGameTickCount())
return GetGameTickCount();
bool lilac_valid_tickcount(int client)
// Tickcount should increment for legit players 99% of the time.
// If it doesn't and tolerance is set to 0, then apply backtrack patch.
if (icvar[CVAR_BACKTRACK_TOLERANCE] == 0)
return (playerinfo_tickcount_prev[client] + 1 == playerinfo_tickcount[client]);
// Tolerance is set, check if the difference in tickcount is over the limit.
diff = (playerinfo_tickcount_prev[client] + 1) - playerinfo_tickcount[client];
return (diff <= icvar[CVAR_BACKTRACK_TOLERANCE]);
void lilac_set_client_in_backtrack_timeout(int client)
// Set the player in backtrack timeout for 1.1 seconds.
playerinfo_time_backtrack[client] = GetGameTime() + 1.1;
playerinfo_tickcount_diff[client] = (playerinfo_tickcount_prev[client] - (GetGameTickCount() - RoundToNearest(GetClientAvgLatency(client, NetFlow_Outgoing) / GetTickInterval()))) + 1;
// Clamp the value due to floating point errors and network variability.
if (playerinfo_tickcount_diff[client] > time_to_ticks(0.2) - 3)
playerinfo_tickcount_diff[client] = time_to_ticks(0.2) - 3;
else if (playerinfo_tickcount_diff[client] < ((time_to_ticks(0.2) * -1) + 3))
playerinfo_tickcount_diff[client] = (time_to_ticks(0.2) * -1) + 3;
bool lilac_is_player_in_backtrack_timeout(int client)
return (GetGameTime() < playerinfo_time_backtrack[client]);
// Todo: I should update this soon...
bool bullettime_can_shoot(int client)
if (!IsPlayerAlive(client))
weapon = GetEntPropEnt(client, Prop_Data, "m_hActiveWeapon");
if (!IsValidEntity(weapon))
if (GetEntPropFloat(client, Prop_Data, "m_flSimulationTime")
>= GetEntPropFloat(weapon, Prop_Data, "m_flNextPrimaryAttack"))
void lilac_detected_noisemaker(int client)
if (playerinfo_banned_flags[client][CHEAT_NOISEMAKER_SPAM])
if (lilac_forward_allow_cheat_detection(client, CHEAT_NOISEMAKER_SPAM) == false)
playerinfo_banned_flags[client][CHEAT_NOISEMAKER_SPAM] = true;
lilac_forward_client_cheat(client, CHEAT_NOISEMAKER_SPAM);
lilac_log_setup_client(client);
Format(line, sizeof(line), "%s is suspected of using unlimited noisemaker cheats.", line);
if (icvar[CVAR_LOG_EXTRA])
// Enable this later if no false positives are reported.
// lilac_ban_client(client, CHEAT_NOISEMAKER_SPAM);
void lilac_detected_anti_duck_delay(int client)
if (playerinfo_banned_flags[client][CHEAT_ANTI_DUCK_DELAY])
if (playerinfo_time_forward[client][CHEAT_ANTI_DUCK_DELAY] > GetGameTime())
if (lilac_forward_allow_cheat_detection(client, CHEAT_ANTI_DUCK_DELAY) == false) {
// Don't spam this forward again for the next 10 seconds.
playerinfo_time_forward[client][CHEAT_ANTI_DUCK_DELAY] = GetGameTime() + 10.0;
playerinfo_banned_flags[client][CHEAT_ANTI_DUCK_DELAY] = true;
lilac_forward_client_cheat(client, CHEAT_ANTI_DUCK_DELAY);
lilac_log_setup_client(client);
Format(line, sizeof(line), "%s was detected and banned for Anti-Duck-Delay.", line);
if (icvar[CVAR_LOG_EXTRA])
lilac_ban_client(client, CHEAT_ANTI_DUCK_DELAY);
void lilac_detected_aimlock(int client)
if (playerinfo_banned_flags[client][CHEAT_AIMLOCK])
// Suspicions reset after 3 minutes.
// This means you need to get two aimlocks within
// three minutes of each other to get a single
if (GetGameTime() - playerinfo_time_aimlock[client] < 180.0)
playerinfo_aimlock_sus[client]++;
playerinfo_aimlock_sus[client] = 1;
playerinfo_time_aimlock[client] = GetGameTime();
if (playerinfo_aimlock_sus[client] < 2)
playerinfo_aimlock_sus[client] = 0;
if (lilac_forward_allow_cheat_detection(client, CHEAT_AIMLOCK) == false)
// Detection expires in 10 minutes.
CreateTimer(600.0, timer_decrement_aimlock, GetClientUserId(client));
lilac_forward_client_cheat(client, CHEAT_AIMLOCK);
// Don't log the first detection.
if (++playerinfo_aimlock[client] < 2)
lilac_log_setup_client(client);
Format(line, sizeof(line),
"%s is suspected of using an aimlock (Detection: %d).",
line, playerinfo_aimlock[client]);
if (icvar[CVAR_LOG_EXTRA] == 2)
if (playerinfo_aimlock[client] >= icvar[CVAR_AIMLOCK]
&& icvar[CVAR_AIMLOCK] >= AIMLOCK_BAN_MIN) {
playerinfo_banned_flags[client][CHEAT_AIMLOCK] = true;
lilac_log_setup_client(client);
Format(line, sizeof(line),
"%s was banned for Aimlock.", line);
if (icvar[CVAR_LOG_EXTRA])
lilac_ban_client(client, CHEAT_AIMLOCK);
void lilac_detected_bhop(int client)
if (playerinfo_banned_flags[client][CHEAT_BHOP])
switch (icvar[CVAR_BHOP]) {
if (playerinfo_bhop[client] < bhop_max[BHOP_SIMPLISTIC])
if (playerinfo_bhop[client] < bhop_max[BHOP_ADVANCED])
else if (playerinfo_bhop[client] < bhop_max[BHOP_SIMPLISTIC]
&& playerinfo_jumps[client] > 15)
if (lilac_forward_allow_cheat_detection(client, CHEAT_BHOP) == false)
playerinfo_banned_flags[client][CHEAT_BHOP] = true;
lilac_forward_client_cheat(client, CHEAT_BHOP);
lilac_log_setup_client(client);
Format(line, sizeof(line),
"%s was detected and banned for Bhop (Jumps Presses: %d | Bhops: %d).",
line, playerinfo_jumps[client] - 1, playerinfo_bhop[client]);
if (icvar[CVAR_LOG_EXTRA])
lilac_ban_client(client, CHEAT_BHOP);
void lilac_detected_angles(int client)
if (playerinfo_banned_flags[client][CHEAT_ANGLES])
if (playerinfo_time_forward[client][CHEAT_ANGLES] > GetGameTime())
if (lilac_forward_allow_cheat_detection(client, CHEAT_ANGLES) == false) {
// Don't spam this forward again for the next 20 seconds.
playerinfo_time_forward[client][CHEAT_ANGLES] = GetGameTime() + 20.0;
playerinfo_banned_flags[client][CHEAT_ANGLES] = true;
lilac_forward_client_cheat(client, CHEAT_ANGLES);
get_player_log_angles(client, 0, true, ang);
lilac_log_setup_client(client);
Format(line, sizeof(line),
"%s was detected and banned for Angle-Cheats (Pitch: %.2f, Yaw: %.2f, Roll: %.2f).",
if (icvar[CVAR_LOG_EXTRA])
lilac_ban_client(client, CHEAT_ANGLES);
void lilac_detected_aimbot(int client, float delta, float td, int flags)
if (playerinfo_banned_flags[client][CHEAT_AIMBOT])
if (lilac_forward_allow_cheat_detection(client, CHEAT_AIMBOT) == false)
// Detection expires in 10 minutes.
CreateTimer(600.0, timer_decrement_aimbot, GetClientUserId(client));
lilac_forward_client_cheat(client, CHEAT_AIMBOT);
// Don't log the first detection.
if (++playerinfo_aimbot[client] < 2)
lilac_log_setup_client(client);
Format(line, sizeof(line),
"%s is suspected of using an aimbot (Detection: %d | Delta: %.0f | TotalDelta: %.0f | Detected:%s%s%s%s%s).",
line, playerinfo_aimbot[client], delta, td,
((flags & AIMBOT_FLAG_SNAP) ? " Aim-Snap" : ""),
((flags & AIMBOT_FLAG_SNAP2) ? " Aim-Snap2" : ""),
((flags & AIMBOT_FLAG_AUTOSHOOT) ? " Autoshoot" : ""),
((flags & AIMBOT_FLAG_REPEAT) ? " Angle-Repeat" : ""),
((td > AIMBOT_MAX_TOTAL_DELTA) ? " Total-Delta" : ""));
if (icvar[CVAR_LOG_EXTRA] == 2)
if (playerinfo_aimbot[client] >= icvar[CVAR_AIMBOT]
&& icvar[CVAR_AIMBOT] >= AIMBOT_BAN_MIN) {
lilac_log_setup_client(client);
Format(line, sizeof(line),
"%s was banned for Aimbot.", line);
if (icvar[CVAR_LOG_EXTRA])
playerinfo_banned_flags[client][CHEAT_AIMBOT] = true;
lilac_ban_client(client, CHEAT_AIMBOT);
public Action OnClientSayCommand(int client, const char[] command, const char[] sArgs)
// Prevent players banned for Chat-Clear from spamming chat.
// Helps legit players see the cheater was banned.
if (playerinfo_banned_flags[client][CHEAT_CHATCLEAR])
public void OnClientSayCommand_Post(int client, const char[] command,
// As far as I know, CSGO is the only game where this doesn't work.
if (!icvar[CVAR_CHAT] || !icvar[CVAR_ENABLE])
// Don't log chat-clear more than once.
if (playerinfo_banned_flags[client][CHEAT_CHATCLEAR])
if (does_string_contain_newline(sArgs)) {
if (lilac_forward_allow_cheat_detection(client, CHEAT_CHATCLEAR) == false)
playerinfo_banned_flags[client][CHEAT_CHATCLEAR] = true;
lilac_forward_client_cheat(client, CHEAT_CHATCLEAR);
lilac_log_setup_client(client);
Format(line, sizeof(line),
"%s was detected and banned for Chat-Clear (Chat message: %s)",
if (icvar[CVAR_LOG_EXTRA])
lilac_ban_client(client, CHEAT_CHATCLEAR);
bool does_string_contain_newline(const char []string)
for (int i = 0; string[i]; i++) {
// Newline or carriage return.
if (string[i] == '\n' || string[i] == 0x0d)
void lilac_aimlock_light_test(int client)
float lastang[3], ang[3];
// Player recently teleported, spawned or taunted. Ignore.
if (GetGameTime() - playerinfo_time_teleported[client] < 3.0)
ind = playerinfo_index[client];
for (int i = 0; i < time_to_ticks(0.5); i++) {
get_player_log_angles(client, ind, false, ang);
// This player has a somewhat big delta,
// test this player for aimlock for 200 seconds.
// Even if we end up flagging more than 5 players
// for this, that's fine as only 5 players
// can be processed in the aimlock check timer.
if (angle_delta(lastang, ang) > 20.0) {
playerinfo_time_process_aimlock[client] = GetGameTime() + 200.0;
bool lilac_is_player_in_aimlock_que(int client)
// Test for aimlock on players who:
return (GetGameTime() < playerinfo_time_process_aimlock[client] // Are in the que.
|| playerinfo_aimlock[client] // Already has a detection.
|| playerinfo_aimbot[client] > 1 // Already have been detected for aimbot twice.
|| GetClientTime(client) < 240.0 // Client just joined the game.
|| (GetGameTime() - playerinfo_time_aimlock[client] < 180.0 && playerinfo_time_aimlock[client] > 1.0)); // Had one aimlock the past three minutes.
void lilac_setup_date_format(const char []format)
strcopy(dateformat, sizeof(dateformat), format);
if (ReplaceString(dateformat, sizeof(dateformat), "{raw}", "", false))
ReplaceString(dateformat, sizeof(dateformat), "%%", "%%%%", false);
ReplaceString(dateformat, sizeof(dateformat), "{year}", "%Y", false);
ReplaceString(dateformat, sizeof(dateformat), "{month}", "%m", false);
ReplaceString(dateformat, sizeof(dateformat), "{day}", "%d", false);
ReplaceString(dateformat, sizeof(dateformat), "{hour}", "%H", false);
ReplaceString(dateformat, sizeof(dateformat), "{hours}", "%H", false);
ReplaceString(dateformat, sizeof(dateformat), "{24hour}", "%H", false);
ReplaceString(dateformat, sizeof(dateformat), "{24hours}", "%H", false);
ReplaceString(dateformat, sizeof(dateformat), "{12hour}", "%I", false);
ReplaceString(dateformat, sizeof(dateformat), "{12hours}", "%I", false);
ReplaceString(dateformat, sizeof(dateformat), "{pm}", "%p", false);
ReplaceString(dateformat, sizeof(dateformat), "{am}", "%p", false);
ReplaceString(dateformat, sizeof(dateformat), "{minute}", "%M", false);
ReplaceString(dateformat, sizeof(dateformat), "{minutes}", "%M", false);
ReplaceString(dateformat, sizeof(dateformat), "{second}", "%S", false);
ReplaceString(dateformat, sizeof(dateformat), "{seconds}", "%S", false);
void lilac_log_setup_client(int client)
char date[512], steamid[64], ip[64];
FormatTime(date, sizeof(date), dateformat, GetTime());
GetClientAuthId(client, AuthId_Steam2, steamid, sizeof(steamid), true);
GetClientIP(client, ip, sizeof(ip), true);
Format(line, sizeof(line),
"%s [Version %s] {Name: \"%N\" | SteamID: %s | IP: %s}",
date, VERSION, client, steamid, ip);
void lilac_log_extra(int client)
char map[128], weapon[64];
GetClientAbsOrigin(client, pos);
GetCurrentMap(map, sizeof(map));
GetClientWeapon(client, weapon, sizeof(weapon));
get_player_log_angles(client, 0, true, ang);
Format(line, sizeof(line),
"\tPos={%.0f,%.0f,%.0f}, Angles={%.5f,%.5f,%.5f}, Map=\"%s\", Team={%d}, Weapon=\"%s\", Latency={Inc:%f,Out:%f}, Loss={Inc:%f,Out:%f}, Choke={Inc:%f,Out:%f}, ConnectionTime={%f seconds}, GameTime={%f seconds}",
map, GetClientTeam(client), weapon,
GetClientAvgLatency(client, NetFlow_Incoming),
GetClientAvgLatency(client, NetFlow_Outgoing),
GetClientAvgLoss(client, NetFlow_Incoming),
GetClientAvgLoss(client, NetFlow_Outgoing),
GetClientAvgChoke(client, NetFlow_Incoming),
GetClientAvgChoke(client, NetFlow_Outgoing),
GetClientTime(client), GetGameTime());
void lilac_log(bool cleanup)
Handle file = OpenFile("addons/sourcemod/logs/lilac.log", "a");
PrintToServer("[Lilac] Cannot open log file.");
// Remove invalid characters.
// This doesn't care about invalid utf-8 formatting,
// only ASCII control characters.
for (int i = 0; line[i]; i++) {
if (line[i] == '\n' || line[i] == 0x0d)
WriteFileLine(file, "%s", line);
void lilac_log_first_time_setup()
// Some admins may not understand how to interpret cheat logs
// correctly, thus, we should warn them so they don't panic
if (!FileExists("addons/sourcemod/logs/lilac.log", false, NULL_STRING)) {
Format(line, sizeof(line),
"=========[Notice]=========\n\
Thank you for installing Little Anti-Cheat %s!\n\
Just a few notes about this Anti-Cheat:\n\n\
If a player is logged as \"suspected\" of using cheats, they are not necessarily cheating.\n\
If the suspicions logged are few and rare, they are likely false positives.\n\
An automatic ban is triggered by 5 or more \"suspicions\" or by one \"detection\".\n\
If you think a ban may be incorrect, please do not hesitate to let me know.\n\n\
That is all, have a wonderful day~\n\n\n", VERSION);
void lilac_ban_client(int client, int cheat)
// Banning has been disabled, don't forward the ban and don't ban.
case CHEAT_ANGLES: { Format(reason, sizeof(reason),
"[Little Anti-Cheat %s] %T", VERSION, "ban_angle", client); }
case CHEAT_CHATCLEAR: { Format(reason, sizeof(reason),
"[Little Anti-Cheat %s] %T", VERSION, "ban_chat_clear", client); }
case CHEAT_CONVAR: { Format(reason, sizeof(reason),
"[Little Anti-Cheat %s] %T", VERSION, "ban_convar", client); }
// It saying "convar violation" for nolerp is intentional.
case CHEAT_NOLERP: { Format(reason, sizeof(reason),
"[Little Anti-Cheat %s] %T", VERSION, "ban_convar", client); }
case CHEAT_BHOP: { Format(reason, sizeof(reason),
"[Little Anti-Cheat %s] %T", VERSION, "ban_bhop", client); }
case CHEAT_AIMBOT: { Format(reason, sizeof(reason),
"[Little Anti-Cheat %s] %T", VERSION, "ban_aimbot", client); }
case CHEAT_AIMLOCK: { Format(reason, sizeof(reason),
"[Little Anti-Cheat %s] %T", VERSION, "ban_aimlock", client); }
case CHEAT_ANTI_DUCK_DELAY: { Format(reason, sizeof(reason),
"[Little Anti-Cheat %s] %T", VERSION, "ban_anti_duck_delay", client); }
case CHEAT_NOISEMAKER_SPAM: { Format(reason, sizeof(reason),
"[Little Anti-Cheat %s] %T", VERSION, "ban_noisemaker", client); }
lilac_forward_client_ban(client, cheat);
if (icvar[CVAR_MA] && NATIVE_EXISTS("MABanPlayer")) {
MABanPlayer(0, client, MA_BAN_STEAM, get_ban_length(cheat), reason);
CreateTimer(5.0, timer_kick, GetClientUserId(client));
if (icvar[CVAR_SB] && NATIVE_EXISTS("SBPP_BanPlayer")) {
SBPP_BanPlayer(0, client, get_ban_length(cheat), reason);
CreateTimer(5.0, timer_kick, GetClientUserId(client));
BanClient(client, get_ban_length(cheat), BANFLAG_AUTO, reason, reason, "lilac", 0);
CreateTimer(5.0, timer_kick, GetClientUserId(client));
int get_entity_noisemaker_type(int itemindex)
case 280: return NOISEMAKER_TYPE_LIMITED; // Black cat.
case 281: return NOISEMAKER_TYPE_LIMITED; // Gremlin.
case 282: return NOISEMAKER_TYPE_LIMITED; // Werewolf.
case 283: return NOISEMAKER_TYPE_LIMITED; // Witch.
case 284: return NOISEMAKER_TYPE_LIMITED; // Banshee.
case 286: return NOISEMAKER_TYPE_LIMITED; // Crazy Laugh.
case 288: return NOISEMAKER_TYPE_LIMITED; // Stabby.
case 362: return NOISEMAKER_TYPE_LIMITED; // Bell.
case 364: return NOISEMAKER_TYPE_LIMITED; // Gong.
case 365: return NOISEMAKER_TYPE_LIMITED; // Koto.
case 493: return NOISEMAKER_TYPE_LIMITED; // Fireworks.
case 542: return NOISEMAKER_TYPE_LIMITED; // Vuvuzela.
case 536: return NOISEMAKER_TYPE_UNLIMITED; // Birthday.
case 673: return NOISEMAKER_TYPE_UNLIMITED; // Winter 2011.
return NOISEMAKER_TYPE_NONE;
public Action timer_kick(Handle timer, int userid)
int client = GetClientOfUserId(userid);
if (is_player_valid(client))
KickClient(client, "%T", "kick_ban_generic", client);
int get_ban_length(int cheat)
return ((ban_length_overwrite[cheat] <= -1) ? icvar[CVAR_BAN_LENGTH] : ban_length_overwrite[cheat]);
void get_player_log_angles(int client, int tick, bool latest, float writeto[3])
i = playerinfo_index[client];
writeto[0] = playerinfo_angles[client][i][0];
writeto[1] = playerinfo_angles[client][i][1];
writeto[2] = playerinfo_angles[client][i][2];
void set_player_log_angles(int client, float ang[3], int tick)
playerinfo_angles[client][i][0] = ang[0];
playerinfo_angles[client][i][1] = ang[1];
playerinfo_angles[client][i][2] = ang[2];
void aim_at_point(const float p1[3], const float p2[3], float writeto[3])
SubtractVectors(p2, p1, writeto);
GetVectorAngles(writeto, writeto);
while (writeto[0] > 90.0)
while (writeto[0] < -90.0)
while (writeto[1] > 180.0)
while (writeto[1] < -180.0)
float angle_delta(float []a1, float []a2)
float p1[3], p2[3], delta;
// We don't care about roll.
delta = GetVectorDistance(p1, p2);
// Normalize maximum 5 times, yaw can sometimes be odd.
while (delta > 180.0 && normal > 0) {
delta = FloatAbs(delta - 360.0);
bool skip_due_to_loss(int client)
// Debate: What percentage should this be at?
// Skip detection if the loss is more than 50%
if (icvar[CVAR_LOSS_FIX])
return GetClientAvgLoss(client, NetFlow_Both) > 0.5;
int time_to_ticks(float time)
return RoundToNearest(time / GetTickInterval());
bool is_player_valid(int client)
return (client >= 1 && client <= MaxClients
&& IsClientConnected(client) && IsClientInGame(client)
&& !IsClientSourceTV(client));
public Action timer_decrement_aimbot(Handle timer, int userid)
int client = GetClientOfUserId(userid);
if (!is_player_valid(client))
if (playerinfo_aimbot[client] > 0)
playerinfo_aimbot[client]--;
public Action timer_decrement_aimlock(Handle timer, int userid)
int client = GetClientOfUserId(userid);
if (!is_player_valid(client))
if (playerinfo_aimlock[client] > 0)
playerinfo_aimlock[client]--;
void lilac_forward_client_cheat(int client, int cheat)
if (forwardhandle == null)
Call_StartForward(forwardhandle);
void lilac_forward_client_ban(int client, int cheat)
if (forwardhandleban == null)
Call_StartForward(forwardhandleban);
bool lilac_forward_allow_cheat_detection(int client, int cheat)
Action result = Plugin_Continue;
if (forwardhandleallow == null)
Call_StartForward(forwardhandleallow);
if (result == Plugin_Continue)