#include <amxmisc>
#include <reapi>
#include <xs>
// Helper macro
#define VALID_PLAYER(%0) (1 <= %0 <= MaxClients)
#define VALID_TEAM(%0) (TEAM_TERRORIST <= get_member(%0, m_iTeam) <= TEAM_CT)
#define FStringNull(%0) (!%0[0] || %0[0] == '0' || strlen(%0) <= 0)
#define IS_MP3(%0) (equal(%0[strlen(%0) - 4], ".mp3"))
#define CVAR(%0) CVAR__%0
enum (<<=1) {
eNotification_SayText = 1,
eNotification_Hud,
eNotification_Sound,
};
enum (<<=1) {
eReset_Button = 1,
eReset_Position,
eReset_Aim,
eReset_SayText,
eReset_Menu,
};
// Cvars
new CVAR__SaveTime;
new CVAR__MinPlayers;
new CVAR__AFKPercent;
new CVAR__NotifyTimes;
new CVAR__NotifyIsAFK;
new CVAR__ResetTime;
new CVAR__OnlyKick;
new Float:CVAR__Fragsub;
new CVAR__Notification;
new CVAR__Notify[64];
new CVAR__Alert[64];
new CVAR__AlertStage;
new CVAR__TransferC4;
new CVAR__MoneySave;
new CVAR__ScenarioMode;
new CVAR__ScenarioModeMessages;
new Float:CVAR__MaxTime;
new CVAR__AFKReason[64];
// Forwards
new g_iForward_Stage;
new g_iForward_Action;
new g_iForward_Toggle;
new g_iForward_Return;
// Vars
new g_pAFKFlag;
new Float:g_LastActivity[33], Float:g_SaveActivity[33];
new Float:g_vecOldOrigin[33][3], Float:g_vecOldViewAngle[33][3];
new g_AFKStage[33];
public plugin_natives()
{
register_native("afkc_is_user_afk", "native_afkc_is_user_afk");
register_native("afkc_get_stage", "native_afkc_get_stage");
register_native("afkc_update_activity", "native_afkc_update_activity");
}
public plugin_precache()
{
register_plugin("AFK Control", "12.08.2025", "@emmajule");
if (!parseINI()) {
set_fail_state("Something went wrong. Check plugin settings!");
return;
}
precache_sound_ex(CVAR(Notify));
precache_sound_ex(CVAR(Alert));
}
public plugin_init()
{
register_dictionary("afk_control.txt");
register_event("TeamInfo", "Event_TeamInfo", "a", "1>0", "2=SPECTATOR");
// register_menu("Show_AfkControlMenu", 1<<0, "Handle_AfkControlMenu");
register_clcmd("menuselect", "clcmd_menuselect");
RegisterHookChain(RG_CBasePlayer_PreThink, "CBasePlayer_PreThink", true);
RegisterHookChain(RG_CBasePlayer_Killed, "CBasePlayer_Killed", false);
RegisterHookChain(RG_CSGameRules_CheckWinConditions, "CSGameRules_CheckWinConditions", true);
g_iForward_Stage = CreateMultiForward("afkc_stage", ET_IGNORE, FP_CELL, FP_FLOAT, FP_VAL_BYREF);
g_iForward_Action = CreateMultiForward("afkc_action", ET_CONTINUE, FP_CELL);
g_iForward_Toggle = CreateMultiForward("afkc_toggle", ET_IGNORE, FP_CELL, FP_CELL);
}
public client_putinserver(id)
{
g_SaveActivity[id] = g_LastActivity[id] = 0.0;
}
/*
public clcmd_hostsay(id)
{
g_LastActivity[id] = 0.0;
}
*/
public clcmd_menuselect(id)
{
if (!(CVAR(ResetTime) & eReset_Menu)) {
return 0;
}
g_LastActivity[id] = 0.0;
return 0;
}
public Event_TeamInfo()
{
new pClient = read_data(1);
if (!VALID_PLAYER(pClient)) {
return;
}
g_SaveActivity[pClient] = g_LastActivity[pClient] = 0.0;
}
public CBasePlayer_Killed(const id, attacker, gib)
{
if (!CVAR(SaveTime)) {
g_SaveActivity[id] = get_gametime() - g_LastActivity[id];
}
g_LastActivity[id] = 0.0;
return HC_CONTINUE;
}
public CBasePlayer_PreThink(const id)
{
// Учет живых игроков
AFK__Think(id);
}
AFK__Think(const id)
{
if (!is_user_alive(id)) {
return;
}
new Float:fGameTime = get_gametime();
if (CheckActivityInGame(id) || !g_LastActivity[id])
{
g_LastActivity[id] = fGameTime;
// iWarnings = 0;
}
if (g_SaveActivity[id] > 0.0)
{
// g_LastActivity[id] = g_LastActivity[id] - (fGameTime - g_SaveActivity[id]);
g_LastActivity[id] = g_LastActivity[id] - g_SaveActivity[id];
g_SaveActivity[id] = 0.0;
}
new currentStage = g_AFKStage[id];
new Float:fLastMovement = fGameTime - g_LastActivity[id];
new AFKStage = min(100, floatround(fLastMovement * 100.0 / CVAR(MaxTime), floatround_tozero));
new bIsWarning = (AFKStage % (100 / CVAR(NotifyTimes)) == 0);
if (currentStage == AFKStage) {
return;
}
else {
// g_AFKStage[id] = AFKStage;
}
// forward afkc_stage(id, Float:lastActivity, &stage)
ExecuteForward(g_iForward_Stage, _, id, g_LastActivity[id], AFKStage);
if (AFKStage < GetAFKTime() <= currentStage)
{
if (!FStringNull(CVAR(Alert)) && currentStage >= CVAR(AlertStage))
{
client_cmd(id, ";mp3 stop");
}
// forward afkc_toggle(id, bool:status)
ExecuteForward(g_iForward_Toggle, _, id, false);
}
g_AFKStage[id] = AFKStage;
if (AFKStage <= 0) {
return;
}
// Иммунитет от действия плагина
if (hasAccess(id, g_pAFKFlag) || get_playersnum() < CVAR(MinPlayers))
{
g_LastActivity[id] = 0.0;
return;
}
if (afkc_is_user_afk(id) && get_member(id, m_bHasC4))
{
if (CVAR(TransferC4) == 1 || CVAR(TransferC4) == 2 && !transfer_c4(id))
{
rg_drop_item(id, "weapon_c4");
}
}
// if (fLastMovement >= CVAR(MaxTime)
if (AFKStage >= 100)
{
// forward afkc_action(id)
ExecuteForward(g_iForward_Action, g_iForward_Return, id);
if (g_iForward_Return > PLUGIN_CONTINUE)
{
// g_LastActivity[id] = 0.0;
return;
}
if (CVAR(OnlyKick))
{
// AFK_KICK = ^3%n ^1был кикнут с сервера, причина:^4 %s
client_print_color(0, id, "%l %l", "AFK_PREFIX", "AFK_KICK", id, CVAR(AFKReason));
kickPlayer(id, CVAR(AFKReason));
return;
}
// Перевод в зрители
set_entvar(id, var_frags, Float:get_entvar(id, var_frags) - CVAR(Fragsub));
rg_disappear(id);
rg_join_team(id, TEAM_SPECTATOR);
// AFK_TRANSFER_TO_SPECTATE = ^3%n^1 переведен в наблюдение за простой!
client_print_color(0, id, "%l %l", "AFK_PREFIX", "AFK_TRANSFER_TO_SPECTATE", id);
if (!CVAR(MoneySave))
{
rg_add_account(id, get_cvar_num("mp_startmoney"), AS_SET, false);
}
if (!FStringNull(CVAR(Alert)))
{
client_cmd(id, ";mp3 stop");
}
// Out of code
return;
}
// В этот момент игрок получает статус AFK
if (AFKStage == GetAFKTime())
{
// forward afkc_toggle(id, bool:status)
ExecuteForward(g_iForward_Toggle, _, id, true);
if (bIsWarning)
{
}
if (CVAR(ScenarioMode) != 0)
{
rg_check_win_conditions();
}
}
if (AFKStage == CVAR(AlertStage))
{
PlaySoundToClients(id, CVAR(Alert));
}
// Уведомления
if (bIsWarning)
{
if (CVAR(NotifyIsAFK) && !afkc_is_user_afk(id)) {
return;
}
if (CVAR(Notification) & eNotification_SayText)
{
// AFK_NOTIFY_CHAT = ^3 Вы не проявляете активность! Через^4 %.0f сек.^3 вы будете кикнуты!
client_print_color(id, print_team_red, "%l %l", "AFK_PREFIX", "AFK_NOTIFY_CHAT", CVAR(MaxTime) - fLastMovement);
}
if (CVAR(Notification) & eNotification_Hud)
{
// AFK_NOTIFY_HUD = Вы не проявляете активность!^nЧерез %.0f сек. вы будете кикнуты!
set_hudmessage(0, 200, 200, -1.0, 0.75, 1, 2.0, 3.0, 0.2, 0.4);
show_hudmessage(id, "%l", "AFK_NOTIFY_HUD", CVAR(MaxTime) - fLastMovement);
}
if (CVAR(Notification) & eNotification_Sound)
{
PlaySoundToClients(id, CVAR(Notify));
}
}
}
public CSGameRules_CheckWinConditions()
{
if (CVAR(ScenarioMode) <= 0) {
return;
}
if (get_member_game(m_bRoundTerminating)) {
return;
}
new AFKCount[TeamName];
new TeamName:team;
// static AFKNotify[TeamName];
for (new i = MaxClients; i > 0; --i)
{
if (!is_user_alive(i)) {
continue;
}
team = get_member(i, m_iTeam);
if (afkc_is_user_afk(i))
{
AFKCount[team]++;
}
else
{
//
AFKCount[team] = -1337;
}
}
if (AFKCount[TEAM_TERRORIST] > 0)
{
for (new i; i < CVAR(ScenarioModeMessages); i++) {
// AFK_TERRORIST_LEFT = Все оставшийся игроки команды^3 TERRORIST^1 находятся^4 AFK !!!
client_print_color(0, print_team_red, "%l %l", "AFK_PREFIX", "AFK_TERRORIST_LEFT", AFKCount[TEAM_TERRORIST]);
}
if (CVAR(ScenarioMode) == 2) {
rg_round_end(get_cvar_float("mp_round_restart_delay"), WINSTATUS_CTS, ROUND_CTS_WIN, .trigger = true);
}
}
if (AFKCount[TEAM_CT] > 0)
{
for (new i; i < CVAR(ScenarioModeMessages); i++) {
// AFK_CT_LEFT = Все оставшийся игроки команды^3 CT^1 находятся^4 AFK !!!
client_print_color(0, print_team_blue, "%l %l", "AFK_PREFIX", "AFK_CT_LEFT", AFKCount[TEAM_CT]);
}
if (CVAR(ScenarioMode) == 2) {
rg_round_end(get_cvar_float("mp_round_restart_delay"), WINSTATUS_TERRORISTS, ROUND_TERRORISTS_WIN, .trigger = true);
}
}
}
transfer_c4(const id)
{
// new aPlayers[32], iCount;
// rg_initialize_player_counts(iCount, _, _, _);
new Float:dist, Float:best_dist;
new player;
for (new i = MaxClients; i > 0; --i)
{
if (i == id) {
continue;
}
if (!is_user_alive(i)) {
continue;
}
if (get_member(i, m_iTeam) != TEAM_TERRORIST) {
continue;
}
if (afkc_is_user_afk(i)) {
continue;
}
dist = rg_entity_range(id, i);
// aPlayers[iCount++] = i;
if (!best_dist || dist < best_dist)
{
best_dist = dist;
player = i;
}
}
if (!player) {
return false;
}
// SortIntegers(aPlayers, iCount, Sort_Random);
// new pClient = aPlayers[random_num(0, iCount - 1)];
return rg_transfer_c4(id, player);
}
parseINI()
{
new path[PLATFORM_MAX_PATH];
get_configsdir(path, charsmax(path));
strcat(path, "/afk_control.ini", charsmax(path));
if (!file_exists(path)) {
return false;
}
new INIParser:parser = INI_CreateParser();
if (parser == Invalid_INIParser) {
return false;
}
INI_SetReaders(parser, "INI_Values");
INI_ParseFile(parser, path);
INI_DestroyParser(parser);
return true;
}
public bool:INI_Values(INIParser:handle, const key[], const value[])
{
// server_print("key: %s | value: %s", key, value);
if (equal(key, "SAVE_TIME"))
{
CVAR(SaveTime) = str_to_num(value);
}
if (equal(key, "IMMUNITY"))
{
// copy(CVAR(Access, 15, value);
g_pAFKFlag = read_flags_ex(value);
}
if (equal(key, "MIN_PLAYERS"))
{
CVAR(MinPlayers) = str_to_num(value);
}
if (equal(key, "MAX_TIME"))
{
CVAR(MaxTime) = str_to_float(value);
}
if (equal(key, "AFK_PERCENT"))
{
CVAR(AFKPercent) = min(99, str_to_num(value));
}
if (equal(key, "WARNINGS"))
{
CVAR(NotifyTimes) = str_to_num(value);
}
if (equal(key, "WARN_ONLY_IF_AFK"))
{
CVAR(NotifyIsAFK) = str_to_num(value);
}
if (equal(key, "RESET_TIME"))
{
CVAR(ResetTime) = read_flags(value);
}
if (equal(key, "KICK"))
{
CVAR(OnlyKick) = str_to_num(value);
}
if (equal(key, "SUB_FRAG"))
{
CVAR(Fragsub) = str_to_float(value);
}
if (equal(key, "NOTIFICATION"))
{
CVAR(Notification) = read_flags(value);
}
if (equal(key, "SAMPLE_NOTIFY"))
{
copy(CVAR(Notify), 63, value);
}
if (equal(key, "ALERT_NOTIFY") && !FStringNull(value))
{
if (!IS_MP3(value))
{
server_print("[AFK Control] Только .mp3 формат доступен для настройки ALERT_NOTIFY");
return true;
}
copy(CVAR(Alert), 63, value);
}
if (equal(key, "ALERT_PERCENT"))
{
CVAR(AlertStage) = str_to_num(value);
}
if (equal(key, "BOMB_TRANSFER"))
{
CVAR(TransferC4) = str_to_num(value);
}
if (equal(key, "MONEY_SAVE"))
{
CVAR(MoneySave) = str_to_num(value);
}
if (equal(key, "SCENARIO_MODE"))
{
CVAR(ScenarioMode) = str_to_num(value);
}
if (equal(key, "SCENARIO_CHAT_TIMES"))
{
CVAR(ScenarioModeMessages) = clamp(str_to_num(value), 1, 5);
}
if (equal(key, "AFK_REASON"))
{
copy(CVAR(AFKReason), 63, value);
}
return true;
}
bool:CheckActivityInGame(const id)
{
new bool:bAFK = true;
static Float:vecPos[3], Float:vecAngle[3];
get_entvar(id, var_origin, vecPos);
get_entvar(id, var_v_angle, vecAngle);
if (CVAR(ResetTime) & eReset_Button)
{
if (get_entvar(id, var_button) > 0)
{
bAFK = false;
}
}
if (CVAR(ResetTime) & eReset_Position)
{
if (!xs_vec_equal(vecPos, g_vecOldOrigin[id]))
{
bAFK = false;
}
}
if (CVAR(ResetTime) & eReset_Aim)
{
new Float:deltaPitch = (g_vecOldViewAngle[id][0] - vecAngle[0]);
new Float:deltaYaw = (g_vecOldViewAngle[id][1] - vecAngle[1]);
if (floatabs(deltaYaw) >= 0.1 && floatabs(deltaPitch) >= 0.1)
{
bAFK = false;
}
}
if (CVAR(ResetTime) & eReset_SayText)
{
if (get_gametime() - Float:get_member(id, m_flLastTalk) < 0.75)
{
bAFK = false;
}
}
xs_vec_copy(vecPos, g_vecOldOrigin[id]);
xs_vec_copy(vecAngle, g_vecOldViewAngle[id]);
return !bAFK;
}
stock hasAccess(id, level)
{
if (level == ADMIN_ALL) {
return 0;
}
// return ((get_user_flags(id) & level) == level);
return (get_user_flags(id) & level);
}
stock read_flags_ex(const flags[])
{
if (FStringNull(flags)) {
return ADMIN_ALL;
}
return read_flags(flags);
}
stock precache_sound_ex(sound[])
{
if (FStringNull(sound)) {
return;
}
if (IS_MP3(sound))
{
precache_generic(sound);
}
else
{
// precache_sound(sound[6]);
precache_sound(sound);
}
}
stock afkc_is_user_afk(const id)
{
if (g_AFKStage[id] >= GetAFKTime()) {
return true;
}
return false;
}
kickPlayer(const id, const reason[] = "")
{
// rh_drop_client(id, reason);
server_cmd("kick #%d ^"%s^"", get_user_userid(id), reason);
server_exec();
}
PlaySoundToClients(const id, const sound[])
{
if (IS_MP3(sound)) {
client_cmd(id, "mp3 play ^"%s^"", sound);
}
else {
client_cmd(id, "spk ^"%s^"", sound);
}
}
GetAFKTime()
{
new perc = CVAR(AFKPercent);
if (perc <= 0) {
perc = 100 / CVAR(NotifyTimes);
}
return perc;
}
// Same as fm_entity_range (fakemeta_util.inc)
stock Float:rg_entity_range(ent1, ent2)
{
static Float:origin1[3], Float:origin2[3];
get_entvar(ent1, var_origin, origin1);
get_entvar(ent2, var_origin, origin2);
return get_distance_f(origin1, origin2);
}
/**
* Check if player is afk
*
* @param id Player index
*
* @return true on success, false otherwise
*/
public native_afkc_is_user_afk(plugin, argc)
{
new id = get_param(1);
if (!VALID_PLAYER(id)) {
return false;
}
return afkc_is_user_afk(id);
}
/**
* Gets the current afk player status stage
*
* @param id Player index
*
* @return AFC stage as a percentage (from 0 to 100)
*/
public native_afkc_get_stage(plugin, argc)
{
new id = get_param(1);
if (!VALID_PLAYER(id)) {
return 0;
}
return g_AFKStage[id];
}
/**
* Instant update of the player's last activity time
*
* @note The second parameter should include local game time (func: get_gametime())
* Use 0 to reset activity time
*
* @note The activity time is calculated for each new frame of the player
*
* @param id Player index
* @param time New time
*
* @return true on success, false otherwise
*/
public native_afkc_update_activity(plugin, argc)
{
new id = get_param(1);
new Float:time = get_param_f(2);
if (!VALID_PLAYER(id)) {
return false;
}
g_LastActivity[id] = time;
return true;
}