#include #include #include #include #include #include "CJump.inc" #include "CStreak.inc" #include "CPlayer.inc" #define MAX_STREAKS 5 #define VALID_MIN_JUMPS 3 #define VALID_MAX_TICKS 5 #define VALID_MIN_VELOCITY 250 int g_aButtons[MAXPLAYERS + 1]; bool g_bOnGround[MAXPLAYERS + 1]; bool g_bHoldingJump[MAXPLAYERS + 1]; bool g_bInJump[MAXPLAYERS + 1]; CPlayer g_aPlayers[MAXPLAYERS + 1]; // Api Handle g_hOnClientDetected; public Plugin myinfo = { name = "AntiBhopCheat", author = "BotoX", description = "Detect all kinds of bhop cheats", version = "0.0", url = "" }; public void OnPluginStart() { LoadTranslations("common.phrases"); RegAdminCmd("sm_stats", Command_Stats, ADMFLAG_GENERIC, "sm_stats <#userid|name>"); RegAdminCmd("sm_streak", Command_Streak, ADMFLAG_GENERIC, "sm_streak <#userid|name> [streak]"); /* Handle late load */ for(int client = 1; client <= MaxClients; client++) { if(IsClientConnected(client)) { if(IsClientConnected(client)) OnClientConnected(client); } } // Api g_hOnClientDetected = CreateGlobalForward("AntiBhopCheat_OnClientDetected", ET_Ignore, Param_Cell, Param_String, Param_String); } public void OnClientConnected(int client) { if(g_aPlayers[client]) { g_aPlayers[client].Dispose(); g_aPlayers[client] = null; } g_aPlayers[client] = new CPlayer(client); } public void OnClientDisconnect(int client) { if(g_aPlayers[client]) { g_aPlayers[client].Dispose(); g_aPlayers[client] = null; } g_bOnGround[client] = false; g_bHoldingJump[client] = false; g_bInJump[client] = false; } public Action OnPlayerRunCmd(int client, int &buttons) { g_aButtons[client] = buttons; return Plugin_Continue; } public void OnPlayerRunCmdPost(int client, int buttons, int impulse, const float vel[3], const float angles[3], int weapon, int subtype, int cmdnum, int tickcount, int seed, const int mouse[2]) { if(!IsPlayerAlive(client)) return; CPlayer Player = g_aPlayers[client]; MoveType ClientMoveType = GetEntityMoveType(client); bool bInWater = GetEntProp(client, Prop_Send, "m_nWaterLevel") >= 2; bool bPrevOnGround = g_bOnGround[client]; bool bOnGround = !bInWater && GetEntityFlags(client) & FL_ONGROUND; bool bPrevHoldingJump = g_bHoldingJump[client]; bool bHoldingJump = view_as(g_aButtons[client] & IN_JUMP); bool bInJump = g_bInJump[client]; float fVecVelocity[3]; GetEntPropVector(client, Prop_Data, "m_vecAbsVelocity", fVecVelocity); fVecVelocity[2] = 0.0; float fVelocity = GetVectorLength(fVecVelocity); if(bInJump && (bInWater || ClientMoveType == MOVETYPE_LADDER || ClientMoveType == MOVETYPE_NOCLIP)) bOnGround = true; if(bOnGround) { if(!bPrevOnGround) { g_bOnGround[client] = true; g_bInJump[client] = false; if(bInJump) OnTouchGround(Player, tickcount, fVelocity); } } else { if(bPrevOnGround) g_bOnGround[client] = false; } if(bHoldingJump) { if(!bPrevHoldingJump && !bOnGround && (bPrevOnGround || bInJump)) { g_bHoldingJump[client] = true; g_bInJump[client] = true; OnPressJump(Player, tickcount, fVelocity, bPrevOnGround); } } else { if(bPrevHoldingJump) { g_bHoldingJump[client] = false; OnReleaseJump(Player, tickcount, fVelocity); } } } // TODO: Release after touch ground void OnTouchGround(CPlayer Player, int iTick, float fVelocity) { //PrintToServer("%d - OnTouchGround", iTick); CStreak CurStreak = Player.hStreak; ArrayList hJumps = CurStreak.hJumps; CJump hJump = hJumps.Get(hJumps.Length - 1); hJump.iEndTick = iTick; hJump.fEndVel = fVelocity; int iLength = hJumps.Length; if(iLength == VALID_MIN_JUMPS) { CurStreak.bValid = true; // Current streak is valid, push onto hStreaks ArrayList ArrayList hStreaks = Player.hStreaks; if(hStreaks.Length == MAX_STREAKS) { // Keep the last 10 streaks CStreak hStreak = hStreaks.Get(0); hStreak.Dispose(); hStreaks.Erase(0); } hStreaks.Push(CurStreak); for(int i = 0; i < iLength - 1; i++) { CJump hJump_ = hJumps.Get(i); DoStats(Player, CurStreak, hJump_); } } else if(iLength > VALID_MIN_JUMPS) { CJump hJump_ = hJumps.Get(hJumps.Length - 2); DoStats(Player, CurStreak, hJump_); } } void OnPressJump(CPlayer Player, int iTick, float fVelocity, bool bLeaveGround) { //PrintToServer("%d - OnPressJump %d", iTick, bLeaveGround); CStreak CurStreak = Player.hStreak; ArrayList hJumps = CurStreak.hJumps; CJump hJump; if(bLeaveGround) { int iPrevJump = -1; // Check if we should start a new streak if(hJumps.Length) { // Last jump was more than VALID_MAX_TICKS ticks ago or not valid and fVelocity < VALID_MIN_VELOCITY hJump = hJumps.Get(hJumps.Length - 1); if(hJump.iEndTick < iTick - VALID_MAX_TICKS || fVelocity < VALID_MIN_VELOCITY) { if(CurStreak.bValid) { CurStreak.iEndTick = hJump.iEndTick; DoStats(Player, CurStreak, hJump); CheckStats(Player, CurStreak); } else CurStreak.Dispose(); CurStreak = new CStreak(); Player.hStreak = CurStreak; hJumps = CurStreak.hJumps; } else { iPrevJump = iTick - hJump.iEndTick; hJump.iNextJump = iPrevJump; } } hJump = new CJump(); hJump.iStartTick = iTick; hJump.fStartVel = fVelocity; if(iPrevJump != -1) hJump.iPrevJump = iPrevJump; hJumps.Push(hJump); } else hJump = hJumps.Get(hJumps.Length - 1); ArrayList hPresses = hJump.hPresses; hPresses.Push(iTick); } void OnReleaseJump(CPlayer Player, int iTick, float fVelocity) { //PrintToServer("%d - OnReleaseJump", iTick); CStreak CurStreak = Player.hStreak; ArrayList hJumps = CurStreak.hJumps; CJump hJump = hJumps.Get(hJumps.Length - 1); ArrayList hPresses = hJump.hPresses; hPresses.Set(hPresses.Length - 1, iTick, 1); } void DoStats(CPlayer Player, CStreak CurStreak, CJump hJump) { int aJumps[3] = {0, 0, 0}; int iPresses = 0; int iTicks = 0; int iLastJunk = 0; CurStreak.iJumps++; Player.iJumps++; ArrayList hPresses = hJump.hPresses; int iStartTick = hJump.iStartTick; int iEndTick = hJump.iEndTick; int iPrevJump = hJump.iPrevJump; int iNextJump = hJump.iNextJump; if(iPrevJump > 0) { int iPerf = iPrevJump - 1; if(iPerf > 2) iPerf = 2; aJumps[iPerf]++; } iPresses = hPresses.Length; iTicks = iEndTick - iStartTick; iLastJunk = iEndTick - hPresses.Get(iPresses - 1, 1); float PressesPerTick = (iPresses * 4.0) / float(iTicks); if(PressesPerTick >= 0.85) { CurStreak.iHyperJumps++; Player.iHyperJumps++; } if(iNextJump != -1 && iNextJump <= 1 && (iLastJunk > 5 || iPresses <= 2) && hJump.fEndVel >= 285.0) { CurStreak.iHackJumps++; Player.iHackJumps++; } int aGlobalJumps[3]; Player.GetJumps(aGlobalJumps); aGlobalJumps[0] += aJumps[0]; aGlobalJumps[1] += aJumps[1]; aGlobalJumps[2] += aJumps[2]; Player.SetJumps(aGlobalJumps); int aStreakJumps[3]; CurStreak.GetJumps(aStreakJumps); aStreakJumps[0] += aJumps[0]; aStreakJumps[1] += aJumps[1]; aStreakJumps[2] += aJumps[2]; CurStreak.SetJumps(aStreakJumps); } void CheckStats(CPlayer Player, CStreak Streak) { int client = Player.iClient; int iStreakJumps = Streak.iJumps; if(iStreakJumps >= 6) { float HackRatio = Streak.iHackJumps / float(iStreakJumps); if(HackRatio >= 0.80) { Player.iHackFlagged += 1; char sBuffer[32]; Format(sBuffer, sizeof(sBuffer), "bhop hack streak %d\n", Player.iHackFlagged); NotifyAdmins(client, sBuffer); } float HyperRatio = Streak.iHyperJumps / float(iStreakJumps); if(HyperRatio >= 0.80) { Player.iHyperFlagged += 1; CPrintToChat(client, "{green}[SM]{default} Turn off your bhop macro/script or hyperscroll!"); CPrintToChat(client, "{green}[SM]{default} Your bhop is {red}turned off{default} until you bhop legit again."); LimitBhop(client, true); } else if(IsBhopLimited(client)) { LimitBhop(client, false); CPrintToChat(client, "{green}[SM]{default} Your bhop is {green}turned on{default} again."); } } int iGlobalJumps = Player.iJumps; if(iGlobalJumps >= 35) { float HackRatio = Player.iHackJumps / float(iGlobalJumps); if(HackRatio >= 0.60 && !Player.bHackGlobal) { Player.bHackGlobal = true; NotifyAdmins(client, "bhop hack global"); } float HyperRatio = Player.iHyperJumps / float(iGlobalJumps); if(HyperRatio >= 0.50) { if(!Player.bHyperGlobal) { Player.bHyperGlobal = true; CPrintToChat(client, "{green}[SM]{default} Turn off your bhop macro/script or hyperscroll!"); CPrintToChat(client, "{green}[SM]{default} Your bhop is {red}turned off{default} until you bhop legit again."); LimitBhop(client, true); } } else if(Player.bHyperGlobal && IsBhopLimited(client)) { LimitBhop(client, false); CPrintToChat(client, "{green}[SM]{default} Your bhop is {green}turned on{default} again."); } } } NotifyAdmins(int client, const char[] sReason) { for(int i = 1; i <= MaxClients; i++) { if(IsClientInGame(i) && !IsFakeClient(i) && CheckCommandAccess(i, "sm_stats", ADMFLAG_GENERIC)) { CPrintToChat(i, "{green}[SM]{default} %L has been detected for {red}%s{default}, please check your console!", client, sReason); FormatStats(i, client); PrintToConsole(client, "%s", "\n"); FormatStreak(i, client, 0); } } char sBuffer[2000]; int len = FormatStats(-1, client, sBuffer, sizeof(sBuffer)); sBuffer[len++] = '\n'; len += FormatStreak(-1, client, 0, sBuffer[len], sizeof(sBuffer) - len); Forward_OnDetected(client, sReason, sBuffer); } public Action Command_Stats(int client, int argc) { if(argc < 1 || argc > 2) { ReplyToCommand(client, "[SM] Usage: sm_stats <#userid|name>"); return Plugin_Handled; } char sArg[65]; char sTargetName[MAX_TARGET_LENGTH]; int iTargets[MAXPLAYERS]; int iTargetCount; bool bIsML; GetCmdArg(1, sArg, sizeof(sArg)); if((iTargetCount = ProcessTargetString(sArg, client, iTargets, MAXPLAYERS, COMMAND_FILTER_NO_MULTI | COMMAND_FILTER_NO_IMMUNITY, sTargetName, sizeof(sTargetName), bIsML)) <= 0) { ReplyToTargetError(client, iTargetCount); return Plugin_Handled; } for(int i = 0; i < iTargetCount; i++) { FormatStats(client, iTargets[i]); PrintToConsole(client, "%s", "\n"); for(int j = 0; j < 3; j++) { FormatStreak(client, iTargets[i], j); PrintToConsole(client, "%s", "\n"); } } return Plugin_Handled; } public Action Command_Streak(int client, int argc) { if(argc < 1 || argc > 2) { ReplyToCommand(client, "[SM] Usage: sm_streak <#userid|name> [streak]"); return Plugin_Handled; } char sArg[65]; char sArg2[8]; char sTargetName[MAX_TARGET_LENGTH]; int iTargets[MAXPLAYERS]; int iTargetCount; bool bIsML; int iStreak = -1; GetCmdArg(1, sArg, sizeof(sArg)); if(argc == 2) { GetCmdArg(2, sArg2, sizeof(sArg2)); iStreak = StringToInt(sArg2); } if((iTargetCount = ProcessTargetString(sArg, client, iTargets, MAXPLAYERS, COMMAND_FILTER_NO_MULTI | COMMAND_FILTER_NO_IMMUNITY, sTargetName, sizeof(sTargetName), bIsML)) <= 0) { ReplyToTargetError(client, iTargetCount); return Plugin_Handled; } for(int i = 0; i < iTargetCount; i++) { FormatStreak(client, iTargets[i], iStreak); } return Plugin_Handled; } int FormatStats(int client, int iTarget, char[] sBuf=0, int len=0) { int iBuf = ConsoleFormat(client, sBuf, len, "[SM] Bunnyhop stats for %L\n", iTarget); CPlayer Player = g_aPlayers[iTarget]; int iGlobalJumps = Player.iJumps; float HyperRatio = Player.iHyperJumps / float(iGlobalJumps); float HackRatio = Player.iHackJumps / float(iGlobalJumps); iBuf += ConsoleFormat(client, sBuf[iBuf], len-iBuf, "Global jumps: %d | Hyper?: %.1f%% | Hack?: %.1f%%\n", iGlobalJumps, HyperRatio * 100.0, HackRatio * 100.0); iBuf += ConsoleFormat(client, sBuf[iBuf], len-iBuf, "bHackGlobal: %d | bHyperGlobal: %d | iHackFlagged: %d | iHyperFlagged: %d\n", Player.bHackGlobal, Player.bHyperGlobal, Player.iHackFlagged, Player.iHyperFlagged); int aGlobalJumps[3]; Player.GetJumps(aGlobalJumps); iBuf += ConsoleFormat(client, sBuf[iBuf], len-iBuf, "Global jumps perf group (1 2 +): %1.f%% %1.f%% %1.f%%\n", (aGlobalJumps[0] / float(iGlobalJumps)) * 100.0, (aGlobalJumps[1] / float(iGlobalJumps)) * 100.0, (aGlobalJumps[2] / float(iGlobalJumps)) * 100.0); return iBuf; } int FormatStreak(int client, int iTarget, int iStreak, char[] sBuf=0, int len=0) { int iBuf = ConsoleFormat(client, sBuf, len, "[SM] Bunnyhop streak %d for %L\n", iStreak, iTarget); CPlayer Player = g_aPlayers[iTarget]; ArrayList hStreaks = Player.hStreaks; CStreak hStreak = Player.hStreak; int iStreaks = hStreaks.Length; // Try showing latest valid streak if(iStreak <= 0 && !hStreak.bValid) { if(iStreaks) hStreak = hStreaks.Get(iStreaks - 1); } else if(iStreak > 0) { if(iStreak > MAX_STREAKS) { iBuf += ConsoleFormat(client, sBuf[iBuf], len-iBuf, "[SM] Streak is out of bounds (max. %d)!\n", MAX_STREAKS); return iBuf; } int iIndex = iStreaks - iStreak; if(iIndex < 0) { iBuf += ConsoleFormat(client, sBuf[iBuf], len-iBuf, "[SM] Only %d streaks are available for this player right now!\n", iStreaks); return iBuf; } hStreak = hStreaks.Get(iIndex); } int iStreakJumps = hStreak.iJumps; float HyperRatio = hStreak.iHyperJumps / float(iStreakJumps); float HackRatio = hStreak.iHackJumps / float(iStreakJumps); iBuf += ConsoleFormat(client, sBuf[iBuf], len-iBuf, "Streak jumps: %d | Hyper?: %.1f%% | Hack?: %.1f%%\n", iStreakJumps, HyperRatio * 100.0, HackRatio * 100.0); int aStreakJumps[3]; hStreak.GetJumps(aStreakJumps); iBuf += ConsoleFormat(client, sBuf[iBuf], len-iBuf, "Streak jumps perf group (1 2 +): %1.f%% %1.f%% %1.f%%\n", (aStreakJumps[0] / float(iStreakJumps)) * 100.0, (aStreakJumps[1] / float(iStreakJumps)) * 100.0, (aStreakJumps[2] / float(iStreakJumps)) * 100.0); iBuf += ConsoleFormat(client, sBuf[iBuf], len-iBuf, "#%2s %7s %5s %8s %4s %6s %s\n", "id", " outvel", " gain", " avgdist", " num", " avg+-", "pattern"); ArrayList hJumps = hStreak.hJumps; float fPrevVel = 0.0; int iPrevEndTick = -1; for(int i = 0; i < hJumps.Length; i++) { CJump hJump = hJumps.Get(i); ArrayList hPresses = hJump.hPresses; float fInVel = hJump.fStartVel; float fOutVel = hJump.fEndVel; int iEndTick = hJump.iEndTick; static char sPattern[512]; int iPatternLen = 0; int iPrevTick = -1; int iTicks; if(iPrevEndTick != -1) { iTicks = hJump.iStartTick - iPrevEndTick; for(int k = 0; k < iTicks && k < 16; k++) sPattern[iPatternLen++] = '|'; } float fAvgDist = 0.0; float fAvgDownUp = 0.0; int iPresses = hPresses.Length; for(int j = 0; j < iPresses; j++) { int aJunkJump[2]; hPresses.GetArray(j, aJunkJump); if(iPrevTick != -1) { iTicks = aJunkJump[0] - iPrevTick; for(int k = 0; k < iTicks && k < 16; k++) sPattern[iPatternLen++] = '.'; fAvgDist += iTicks; } sPattern[iPatternLen++] = '^'; iTicks = aJunkJump[1] - aJunkJump[0]; for(int k = 0; k < iTicks && k < 16; k++) sPattern[iPatternLen++] = ','; fAvgDownUp += iTicks; sPattern[iPatternLen++] = 'v'; iPrevTick = aJunkJump[1]; } fAvgDist /= iPresses; fAvgDownUp /= iPresses; iTicks = iEndTick - iPrevTick; for(int k = 0; k < iTicks && k < 16; k++) sPattern[iPatternLen++] = '.'; sPattern[iPatternLen++] = '|'; sPattern[iPatternLen++] = '\0'; if(fPrevVel == 0.0) fPrevVel = fInVel; iBuf += ConsoleFormat(client, sBuf[iBuf], len-iBuf, "#%2d %7.1f %4d%% %8.2f %4d %6.2f %s\n", i, fOutVel, fPrevVel == 0.0 ? 100 : RoundFloat((fOutVel / fPrevVel) * 100.0 - 100.0), fAvgDist, iPresses, fAvgDownUp, sPattern); iPrevEndTick = iEndTick; fPrevVel = fOutVel; } return iBuf; } bool Forward_OnDetected(int client, const char[] reason, const char[] stats) { Call_StartForward(g_hOnClientDetected); Call_PushCell(client); Call_PushString(reason); Call_PushString(stats); Call_Finish(); } stock int ConsoleFormat(int client, char[] buffer, int maxlength, const char[] format, any ...) { if(client >= 1 && client <= MAXPLAYERS) { char sBuf[1024]; sBuf[VFormat(sBuf, sizeof(sBuf), format, 5) - 1] = 0; PrintToConsole(client, "%s", sBuf); } if(maxlength > 0) return VFormat(buffer, maxlength, format, 5); return 0; }