Skip to content

Commit

Permalink
v1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
shavitush committed Apr 9, 2016
1 parent b974eb7 commit f1206b0
Show file tree
Hide file tree
Showing 2 changed files with 306 additions and 2 deletions.
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
# AFK-Kicker
Kicks AFK players after verifying they're actually AFK.
If the player doesn't do anything difference within X seconds, he will be prompted with a captcha-like random menu. Every menu option except for one ('Don't kick me!'), will kick the player as he was verified as a bot/AFK. He will also be kicked if he chooses nothing within Y seconds.
Durations (X/Y) are definable. Check `cfg/sourcemod/plugin.afk_kicker.cfg` for CVars.
If the player doesn't do anything difference within X seconds since the round started, he will be prompted with a captcha-like random menu. Every menu option except for one ('Don't kick me!'), will kick the player as he was verified as a bot/AFK. He will also be kicked if he chooses nothing within Y seconds.
Durations (X/Y) are definable. Check `cfg/sourcemod/plugin.afk_kicker.cfg` for CVars.
Also respects admins (`b` flag or `afk-kicker-immunity` override).
Will also log kicks to `addons/sourcemod/logs/afk_kicker.log`.

# Note
This plugin is for servers with rounds, such as casual and jailbreak servers.
The plugin will most likely not function properly in MG/KZ/bhop/deathmatch!

# TODO
- [ ] Make 'matches' bitwise for modularity purposes.

# API
Natives:
- [ ] `AFKKicker_SendVerification(int client, int matches)`

Forwards:
- [ ] `AFKKicker_OnKick(int client, int matches)`
- [ ] `AFKKicker_OnVerification(int client, int matches, bool modular)`
291 changes: 291 additions & 0 deletions afk_kicker.sp
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
#include <sourcemod>
#include <sdktools>

#pragma semicolon 1
#pragma newdecls required

#define PLUGIN_VERSION "1.0"

bool gB_Late;

public Plugin myinfo =
{
name = "Simple AFK Kicker",
author = "shavit",
description = "Checks for AFK players and kicks them if they fail a verification.",
version = PLUGIN_VERSION,
url = "http://forums.alliedmods.net/member.php?u=163134"
}

float gF_Position[MAXPLAYERS+1][3];
float gF_Angles[MAXPLAYERS+1][3];
int gI_Buttons[MAXPLAYERS+1];
int gI_Matches[MAXPLAYERS+1];

Handle gT_RoundStart = null;

ConVar gCV_AdminImmune = null;
ConVar gCV_WaitTime = null;
ConVar gCV_Verify = null;
ConVar gCV_CaptchaTime = null;
ConVar gCV_Matches = null;
ConVar gCV_Logging = null;

char gS_LogFile[1024];

public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max)
{
gB_Late = late;

return APLRes_Success;
}

public void OnPluginStart()
{
if(gB_Late)
{
for(int i = 1; i <= MaxClients; i++)
{
OnClientPutInServer(i);
}
}

HookEvent("round_start", Round_Start);
HookEvent("round_end", Round_End);

CreateConVar("afk_kicker_version", PLUGIN_VERSION, "Plugin version.", FCVAR_PLUGIN|FCVAR_NOTIFY|FCVAR_DONTRECORD);

gCV_AdminImmune = CreateConVar("afk_kicker_admin_immunity", "1", "Are admins immunable to AFK kicks?", FCVAR_PLUGIN);
gCV_WaitTime = CreateConVar("afk_kicker_wait_time", "30.0", "Time to wait since the round starts before verifying players.", FCVAR_PLUGIN);
gCV_Verify = CreateConVar("afk_kicker_verify", "1", "Verify if a player is AFK by sending a captcha-like menu?\nSetting to 0 will result in an instant kick upon suspicion of an AFK player and may cause false positives.", FCVAR_PLUGIN);
gCV_CaptchaTime = CreateConVar("afk_kicker_captcha_time", "12", "How much time will a player have to answer the captcha?", FCVAR_PLUGIN);
gCV_Matches = CreateConVar("afk_kicker_matches_required", "2", "Amount of matches (same data since the round starts) required before sending verifications.\nMatches:\nKey presses, crosshair position and player position.", FCVAR_PLUGIN);
gCV_Logging = CreateConVar("afk_kicker_logging", "1", "Log kicks to \"addons/sourcemod/logs/afk_kicker.log\"?", FCVAR_PLUGIN);

AutoExecConfig();

BuildPath(Path_SM, gS_LogFile, 1024, "logs/afk_kicker.log");
}

public void OnClientPutInServer(int client)
{
gF_Position[client] = view_as<float>({0.0, 0.0, 0.0});
gF_Angles[client] = view_as<float>({0.0, 0.0, 0.0});
gI_Buttons[client] = 0;
gI_Matches[client] = 0;
}

public void Round_Start(Handle event, const char[] name, bool dB)
{
if(gT_RoundStart != null)
{
delete gT_RoundStart;
}

for(int i = 1; i <= MaxClients; i++)
{
if(IsValidClient(i, true))
{
GetClientAbsOrigin(i, gF_Position[i]);
GetClientEyeAngles(i, gF_Angles[i]);

gI_Buttons[i] = GetClientButtons(i);
gI_Matches[i] = 0;
}
}

gT_RoundStart = CreateTimer(gCV_WaitTime.FloatValue, Timer_AFKCheck);
}

public Action Timer_AFKCheck(Handle Timer)
{
for(int i = 1; i <= MaxClients; i++)
{
if(IsValidClient(i, true))
{
if(gCV_AdminImmune.BoolValue && CheckCommandAccess(i, "afk-kicker-immunity", ADMFLAG_GENERIC))
{
continue;
}

float fPosition[3];
GetClientAbsOrigin(i, fPosition);

float fAngles[3];
GetClientEyeAngles(i, fAngles);

int iButtons = GetClientButtons(i);

int iMatches = 0;

if(bVectorsEqual(fPosition, gF_Position[i]))
{
iMatches++;
}

if(bVectorsEqual(fAngles, gF_Angles[i]))
{
iMatches++;
}

if(iButtons == gI_Buttons[i])
{
iMatches++;
}

gI_Matches[i] = iMatches;

if(iMatches >= gCV_Matches.IntValue)
{
if(gCV_Verify.BoolValue)
{
PopupAFKMenu(i, gCV_CaptchaTime.IntValue);
}

else
{
NukeClient(i, true, gI_Matches[i], "(instant kick - no verification menu)");
}
}
}
}

gT_RoundStart = null;

return Plugin_Stop;
}

public void PopupAFKMenu(int client, int time)
{
Menu m = new Menu(MenuHandler_AFKVerification);

m.SetTitle("[AFK kicker] You there?");

AddKickItemsToMenu(m, GetRandomInt(1, 4));
m.AddItem("stay", "Yes - Don't kick me!");
AddKickItemsToMenu(m, GetRandomInt(2, 3));

m.ExitButton = false;

m.Display(client, time);
}

public int MenuHandler_AFKVerification(Menu m, MenuAction a, int p1, int p2)
{
switch(a)
{
case MenuAction_Select:
{
char buffer[8];
m.GetItem(p2, buffer, 8);

if(StrEqual(buffer, "stay"))
{
PrintHintText(p1, "AFK verification succeed!\nYou will not get kicked.");
}

else
{
NukeClient(p1, true, gI_Matches[p1], "(failed captcha)");
}
}

case MenuAction_Cancel:
{
// no response
if(p2 == MenuCancel_Timeout)
{
NukeClient(p1, true, gI_Matches[p1], "(did not reply to captcha)");
}
}

case MenuAction_End:
{
delete m;
}

}

return 0;
}

public void NukeClient(int client, bool bLog, int iMatches, const char[] sLog)
{
if(IsValidClient(client))
{
KickClient(client, "You were kicked for being AFK.");

if(gCV_Logging.BoolValue && bLog)
{
LogToFile(gS_LogFile, "%L - Kicked for being AFK. (Matches: %d) %s", client, iMatches, sLog);
}
}
}

public void Round_End(Handle event, const char[] name, bool dB)
{
if(gT_RoundStart != null)
{
delete gT_RoundStart;
gT_RoundStart = null;
}
}

public void AddKickItemsToMenu(Menu m, int amount)
{
char sJunk[16];

for(int i = 1; i <= amount; i++)
{
GetRandomString(sJunk, 16);

m.AddItem("kick", sJunk);
}
}

stock bool IsValidClient(int client, bool bAlive = false)
{
return (client >= 1 && client <= MaxClients && IsClientConnected(client) && IsClientInGame(client) && !IsClientSourceTV(client) && (!bAlive || IsPlayerAlive(client)));
}

stock bool bVectorsEqual(float[3] v1, float[3] v2)
{
return (v1[0] == v2[0] && v1[1] == v2[1] && v1[2] == v2[2]);
}

// I took this one from smlib iirc and converted to the transitional syntax, thanks.
public void GetRandomString(char[] buffer, int size)
{
int random;
int len;
size--;

int length = 16;
char chrs[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234556789";

if(chrs[0] != '\0')
{
len = strlen(chrs) - 1;
}

int n = 0;

while(n < length && n < size)
{
if(chrs[0] == '\0')
{
random = GetRandomInt(33, 126);
buffer[n] = random;
}

else
{
random = GetRandomInt(0, len);
buffer[n] = chrs[random];
}

n++;
}

buffer[length] = '\0';
}

0 comments on commit f1206b0

Please sign in to comment.