/*
This plugin is designed to control players with high ping/loss
Данный плагин предназначен для контроля игроков с высоким пингом/потерями
*/
/* Requirements:
* AMXX 1.9.0 or above
* ReAPI
*/
/* Changelog:
1.0 (22.12.2022) by mx?!:
* First release
1.1 (14.01.2023) by mx?!:
* Added cvars 'ping_dec_ping_warns', 'ping_warn_fluctuation', 'ping_max_fluctuation_warns', 'ping_dec_fluctuation_warns', 'ping_average_count'
1.2 (23.01.2023) by mx?!:
* Added cvar 'ping_ema_mode' (thx to wopox1337)
1.3 (16.11.2024) by mx?!:
* Added cvar 'ping_kick_per_cycle' (How many players can be kicked in one check) [thx to @GALAXY009]
1.4 (23.11.2024) by mx?!:
* Added cvar 'ping_check_seconds' (Player is checked only for the first # seconds)
* Added cvar 'ping_log_kicks' (Log kicks to specified log file)
* Added "0" cvar value for `ping_warn_loss` (Ability to disable loss checking)
* Default value for 'ping_warn_loss' is set to "0" (Loss check disabled by default)
* Now the loss calculation also follows the logic of `ping_ema_mode` and `ping_average_count`
* The ping/loss counter has been separated. Now warnings are counted independently.
* Added cvar `ping_checks_enabled` (Ability to globally disable checks)
1.5 (27.11.2024) by mx?!:
* Kick reason added to logging
*/
new const PLUGIN_VERSION[] = "1.5"
#include amxmodx
#include reapi
// Create cvar config in 'configs/plugins' and run it?
//
// Создавать конфиг с кварами в 'configs/plugins', и запускать его?
#define AUTO_CFG
// Minimal ping tests count for EMA mode (cvar 'ping_ema_mode')
// Do not set it lower than 1
//
// Минимальное кол-во тестов для режима средней скользящей (квар 'ping_ema_mode')
// Не задавать значение меньше 1
#define MIN_EMA_TESTS 3
#define MAX_REASON_LEN 32
enum _:CVAR_ENUM {
CVAR__CHECKS_ENABLED,
Float:CVAR_F__CHECK_INTERVAL,
CVAR__WARN_PING,
CVAR__WARN_LOSS,
CVAR__MAX_WARNS,
CVAR__BAN_MINS,
CVAR__NOTICE_PUNISH,
CVAR__IMMUNITY_FLAG[32],
CVAR__DEC_PING_WARNS,
CVAR__WARN_FLUCTUATION,
CVAR__MAX_FLUCTUATION_WARNS,
CVAR__DEC_FLUCTUATION_WARNS,
CVAR__AVERAGE_COUNT,
CVAR__EMA_MODE,
CVAR__KICK_PER_CYCLE,
CVAR__LOG_KICKS[64],
CVAR__CHECK_SECONDS
}
new g_eCvar[CVAR_ENUM], g_iPingWarns[MAX_PLAYERS + 1], g_iPingSum[MAX_PLAYERS + 1], g_iTests[MAX_PLAYERS + 1]
new g_iLastPing[MAX_PLAYERS + 1], g_iFluctuationWarns[MAX_PLAYERS + 1], g_iDecFluctCounter[MAX_PLAYERS + 1]
new g_iDecPingCounter[MAX_PLAYERS + 1], Float:g_fPlayerPingEMA[MAX_PLAYERS + 1], Float:g_fPlayerLossEMA[MAX_PLAYERS + 1]
new g_iLossSum[MAX_PLAYERS + 1], g_iLossWarns[MAX_PLAYERS + 1], g_iDecLossCounter[MAX_PLAYERS + 1]
public plugin_init() {
register_plugin("Ping Control", PLUGIN_VERSION, "mx?!")
register_dictionary("ping_control.txt")
func_RegCvars()
set_task(4.0, "func_SetTask")
}
func_RegCvars() {
bind_cvar_num( "ping_checks_enabled", "1",
.desc = "Включить проверки (1) или выключить (0) ?",
.bind = g_eCvar[CVAR__CHECKS_ENABLED]
);
bind_cvar_float( "ping_time_check", "10",
.has_min = true, .min_val = 1.0,
.desc = "Интервал между проверками (в секундах)",
.bind = g_eCvar[CVAR_F__CHECK_INTERVAL]
);
bind_cvar_num( "ping_warn_ping", "120",
.desc = "Если пинг игрока # или выше, игрок получает предупреждение",
.bind = g_eCvar[CVAR__WARN_PING]
);
bind_cvar_num( "ping_warn_loss", "0",
.has_min = true, .min_val = 0.0,
.desc = "Если потери пакетов игрока # или выше, игрок получает предупреждение (0 - выкл.)",
.bind = g_eCvar[CVAR__WARN_LOSS]
);
bind_cvar_num( "ping_max_warns", "3",
.has_min = true, .min_val = 1.0,
.desc = "Через сколько предупреждений игрок будет наказан",
.bind = g_eCvar[CVAR__MAX_WARNS]
);
bind_cvar_num( "ping_ban_mins", "1",
.has_min = true, .min_val = 0.0,
.desc = "На сколько минут банить игрока (0 - кикать)",
.bind = g_eCvar[CVAR__BAN_MINS]
);
bind_cvar_num( "ping_notice_punish", "1",
.has_min = true, .min_val = 0.0,
.has_max = true, .max_val = 1.0,
.desc = "Включить оповещение в чат о наказании игрока",
.bind = g_eCvar[CVAR__NOTICE_PUNISH]
);
bind_cvar_string( "ping_immunity_flag", "",
.desc = "Флаги иммунитета к наказанию (требуется любой из; ^"^" - выкл.)",
.bind = g_eCvar[CVAR__IMMUNITY_FLAG], .maxlen = charsmax(g_eCvar[CVAR__IMMUNITY_FLAG])
);
bind_cvar_num( "ping_check_seconds", "0",
.has_min = true, .min_val = 0.0,
.desc = "Проверять игрока только первые # секунд после входа на сервер (0 - проверять всё время)",
.bind = g_eCvar[CVAR__CHECK_SECONDS]
);
bind_cvar_num( "ping_dec_ping_warns", "6",
.desc = "Уменьшать счётчик предупреждений ping/loss каждые # успешных проверок игрока (0 - не уменьшать)",
.bind = g_eCvar[CVAR__DEC_PING_WARNS]
);
bind_cvar_num( "ping_warn_fluctuation", "60",
.desc = "Если скачок пинга игрока # или выше, игрок получает предупреждение",
.bind = g_eCvar[CVAR__WARN_FLUCTUATION]
);
bind_cvar_num( "ping_max_fluctuation_warns", "5",
.has_min = true, .min_val = 0.0,
.desc = "Через сколько предупреждений игрок будет наказан (0 - выкл.)",
.bind = g_eCvar[CVAR__MAX_FLUCTUATION_WARNS]
);
bind_cvar_num( "ping_dec_fluctuation_warns", "6",
.desc = "Уменьшать счётчик скачков пинга каждые # успешных проверок игрока (0 - не уменьшать)",
.bind = g_eCvar[CVAR__DEC_FLUCTUATION_WARNS]
);
bind_cvar_num( "ping_average_count", "0",
.desc = "Режим подсчёта по среднему пингу (как у h1k3). # - кол-во проверок до начала расчёта (0 - выкл.)",
.bind = g_eCvar[CVAR__AVERAGE_COUNT]
);
// https://gist.github.com/wopox1337/41b7f97e49f3fceb707fba1031edb7d6
// https://ru.wikipedia.org/wiki/%D0%A1%D0%BA%D0%BE%D0%BB%D1%8C%D0%B7%D1%8F%D1%89%D0%B0%D1%8F_%D1%81%D1%80%D0%B5%D0%B4%D0%BD%D1%8F%D1%8F
// https://youtu.be/3-4CwYfphXc
bind_cvar_num( "ping_ema_mode", "0",
.desc = "Использовать среднее скользящее для сглаживания скачков при расчёте пинга?",
.bind = g_eCvar[CVAR__EMA_MODE]
);
bind_cvar_num( "ping_kick_per_cycle", "1",
.has_min = true, .min_val = 1.0,
.desc = "Сколько игроков можно кикнуть за одну проверку",
.bind = g_eCvar[CVAR__KICK_PER_CYCLE]
);
bind_cvar_string( "ping_log_kicks", "ping_control.log",
.desc = "Логировать кики в указанный файл в 'amxmodx/logs' (^"^" - выкл.)",
.bind = g_eCvar[CVAR__LOG_KICKS], .maxlen = charsmax(g_eCvar[CVAR__LOG_KICKS])
);
#if defined AUTO_CFG
AutoExecConfig(/*.name = "PluginName"*/)
#endif
}
public func_SetTask() {
set_task(g_eCvar[CVAR_F__CHECK_INTERVAL], "task_Check")
}
public task_Check() {
func_SetTask()
if(!g_eCvar[CVAR__CHECKS_ENABLED]) {
return
}
new pPlayers[MAX_PLAYERS], iPlCount, pPlayer, szReason[MAX_REASON_LEN]
new bitImmunity = read_flags(g_eCvar[CVAR__IMMUNITY_FLAG])
get_players(pPlayers, iPlCount, "ch")
for(new i, iPing, iLoss, iPunishCount; i < iPlCount; i++) {
pPlayer = pPlayers[i]
if(get_user_flags(pPlayer) & bitImmunity) {
continue
}
if(g_eCvar[CVAR__CHECK_SECONDS] && get_user_time(pPlayer) > g_eCvar[CVAR__CHECK_SECONDS]) {
continue
}
g_iTests[pPlayer]++
GetUserPing(pPlayer, iPing, iLoss)
if(CheckPing(pPlayer, iPing, iLoss, szReason) || CheckFluctuation(pPlayer, iPing, szReason)) {
PunishPlayer(pPlayer, iPing, iLoss, szReason)
if(++iPunishCount >= g_eCvar[CVAR__KICK_PER_CYCLE]) {
return
}
}
}
}
GetUserPing(pPlayer, &iPing, &iLoss) {
get_user_ping(pPlayer, iPing, iLoss)
if(!g_eCvar[CVAR__EMA_MODE]) {
return
}
static Float:fAlpha
fAlpha = 2.0 / g_iTests[pPlayer]
g_fPlayerPingEMA[pPlayer] = (fAlpha * iPing) + (1.0 - fAlpha) * g_fPlayerPingEMA[pPlayer]
g_fPlayerLossEMA[pPlayer] = (fAlpha * iLoss) + (1.0 - fAlpha) * g_fPlayerLossEMA[pPlayer]
if(g_iTests[pPlayer] < MIN_EMA_TESTS) {
iPing = 0
iLoss = 0
return
}
iPing = floatround(g_fPlayerPingEMA[pPlayer])
iLoss = floatround(g_fPlayerLossEMA[pPlayer])
}
bool:CheckPing(pPlayer, iPing, iLoss, szReason[MAX_REASON_LEN]) {
if(g_eCvar[CVAR__AVERAGE_COUNT]) {
return CheckAveragePing(pPlayer, iPing, iLoss, szReason)
}
return CheckInstantPing(pPlayer, iPing, iLoss, szReason)
}
bool:CheckAveragePing(pPlayer, iPing, iLoss, szReason[MAX_REASON_LEN]) {
g_iPingSum[pPlayer] += iPing
g_iLossSum[pPlayer] += iLoss
if(g_iTests[pPlayer] < g_eCvar[CVAR__AVERAGE_COUNT]) {
return false
}
if(g_eCvar[CVAR__EMA_MODE]) {
if(iPing >= g_eCvar[CVAR__WARN_PING]) {
copy(szReason, charsmax(szReason), "average ping [ema]")
return true
}
if(g_eCvar[CVAR__WARN_LOSS] && iLoss >= g_eCvar[CVAR__WARN_LOSS]) {
copy(szReason, charsmax(szReason), "average loss [ema]")
return true
}
}
else {
if(g_iPingSum[pPlayer] / g_iTests[pPlayer] >= g_eCvar[CVAR__WARN_PING]) {
copy(szReason, charsmax(szReason), "average ping")
return true
}
if(g_eCvar[CVAR__WARN_LOSS] && g_iLossSum[pPlayer] / g_iTests[pPlayer] >= g_eCvar[CVAR__WARN_LOSS]) {
copy(szReason, charsmax(szReason), "average loss")
return true
}
}
return false
}
bool:CheckInstantPing(pPlayer, iPing, iLoss, szReason[MAX_REASON_LEN]) {
new bool:bHighPing = (iPing >= g_eCvar[CVAR__WARN_PING])
new bool:bHighLoss = (g_eCvar[CVAR__WARN_LOSS] && iLoss >= g_eCvar[CVAR__WARN_LOSS])
if(!bHighPing) {
DecrementPingWarns(pPlayer)
}
if(!bHighLoss) {
DecrementLossWarns(pPlayer)
}
if(bHighPing && ++g_iPingWarns[pPlayer] >= g_eCvar[CVAR__MAX_WARNS]) {
copy(szReason, charsmax(szReason), "instant ping")
return true
}
if(bHighLoss && ++g_iLossWarns[pPlayer] >= g_eCvar[CVAR__MAX_WARNS]) {
copy(szReason, charsmax(szReason), "instant loss")
return true
}
return false
}
DecrementPingWarns(pPlayer) {
if(g_iPingWarns[pPlayer] && g_eCvar[CVAR__DEC_PING_WARNS] && ++g_iDecPingCounter[pPlayer] >= g_eCvar[CVAR__DEC_PING_WARNS]) {
g_iPingWarns[pPlayer]--
g_iDecPingCounter[pPlayer] = 0
}
}
DecrementLossWarns(pPlayer) {
if(g_iLossWarns[pPlayer] && g_eCvar[CVAR__DEC_PING_WARNS] && ++g_iDecLossCounter[pPlayer] >= g_eCvar[CVAR__DEC_PING_WARNS]) {
g_iLossWarns[pPlayer]--
g_iDecLossCounter[pPlayer] = 0
}
}
bool:CheckFluctuation(pPlayer, iPing, szReason[MAX_REASON_LEN]) {
if(!g_eCvar[CVAR__MAX_FLUCTUATION_WARNS]) {
return false
}
if(!g_iLastPing[pPlayer]) {
g_iLastPing[pPlayer] = iPing
return false
}
new iOldLastPing = g_iLastPing[pPlayer]
g_iLastPing[pPlayer] = iPing
if(abs(iOldLastPing - iPing) < g_eCvar[CVAR__WARN_FLUCTUATION]) {
if(g_iFluctuationWarns[pPlayer] && g_eCvar[CVAR__DEC_FLUCTUATION_WARNS] && ++g_iDecFluctCounter[pPlayer] >= g_eCvar[CVAR__DEC_FLUCTUATION_WARNS]) {
g_iFluctuationWarns[pPlayer]--
g_iDecFluctCounter[pPlayer] = 0
}
return false
}
if(++g_iFluctuationWarns[pPlayer] >= g_eCvar[CVAR__MAX_FLUCTUATION_WARNS]) {
copy(szReason, charsmax(szReason), "ping fluctuation")
return true
}
return false
}
PunishPlayer(pPlayer, iPing, iLoss, const szReason[]) {
if(g_eCvar[CVAR__NOTICE_PUNISH]) {
client_print_color(0, pPlayer, "%L", LANG_PLAYER, "PC__KICK_ALL", pPlayer)
}
if(g_eCvar[CVAR__LOG_KICKS][0]) {
log_to_file(g_eCvar[CVAR__LOG_KICKS], "PingKick: %N [tests %i, current ping %i/%i, current loss %i/%i, reason '%s']", pPlayer, g_iTests[pPlayer], iPing, g_eCvar[CVAR__WARN_PING], iLoss, g_eCvar[CVAR__WARN_LOSS], szReason)
}
if(g_eCvar[CVAR__BAN_MINS]) {
new szIP[MAX_IP_LENGTH]
get_user_ip(pPlayer, szIP, charsmax(szIP), .without_port = 1)
set_task(1.0, "task_BanIP", g_eCvar[CVAR__BAN_MINS], szIP, sizeof(szIP))
}
server_cmd("kick #%i ^"%L^"", get_user_userid(pPlayer), pPlayer, "PC__KICK_INFO")
}
public task_BanIP(const szIP[], iBanMins) {
server_cmd("addip %i %s", iBanMins, szIP)
}
public client_connect(pPlayer) {
g_iPingWarns[pPlayer] = g_iLossWarns[pPlayer] = 0
g_iPingSum[pPlayer] = g_iLossSum[pPlayer] = 0
g_iTests[pPlayer] = 0
g_iLastPing[pPlayer] = g_iFluctuationWarns[pPlayer] = g_iDecFluctCounter[pPlayer] = 0
g_iDecPingCounter[pPlayer] = g_iDecLossCounter[pPlayer] = 0
g_fPlayerPingEMA[pPlayer] = 1.0
g_fPlayerLossEMA[pPlayer] = 1.0
}
stock bind_cvar_num(const cvar[], const value[], flags = FCVAR_NONE, const desc[] = "", bool:has_min = false, Float:min_val = 0.0, bool:has_max = false, Float:max_val = 0.0, &bind) {
bind_pcvar_num(create_cvar(cvar, value, flags, desc, has_min, min_val, has_max, max_val), bind)
}
stock bind_cvar_float(const cvar[], const value[], flags = FCVAR_NONE, const desc[] = "", bool:has_min = false, Float:min_val = 0.0, bool:has_max = false, Float:max_val = 0.0, &Float:bind) {
bind_pcvar_float(create_cvar(cvar, value, flags, desc, has_min, min_val, has_max, max_val), bind)
}
stock bind_cvar_string(const cvar[], const value[], flags = FCVAR_NONE, const desc[] = "", bool:has_min = false, Float:min_val = 0.0, bool:has_max = false, Float:max_val = 0.0, bind[], maxlen) {
bind_pcvar_string(create_cvar(cvar, value, flags, desc, has_min, min_val, has_max, max_val), bind, maxlen)
}