diff options
Diffstat (limited to 'sourcemod/scripting/gokz-anticheat')
| -rw-r--r-- | sourcemod/scripting/gokz-anticheat/api.sp | 174 | ||||
| -rw-r--r-- | sourcemod/scripting/gokz-anticheat/bhop_tracking.sp | 336 | ||||
| -rw-r--r-- | sourcemod/scripting/gokz-anticheat/commands.sp | 76 |
3 files changed, 586 insertions, 0 deletions
diff --git a/sourcemod/scripting/gokz-anticheat/api.sp b/sourcemod/scripting/gokz-anticheat/api.sp new file mode 100644 index 0000000..7f99724 --- /dev/null +++ b/sourcemod/scripting/gokz-anticheat/api.sp @@ -0,0 +1,174 @@ +static GlobalForward H_OnPlayerSuspected; + + + +// =====[ FORWARDS ]===== + +void CreateGlobalForwards() +{ + H_OnPlayerSuspected = new GlobalForward("GOKZ_AC_OnPlayerSuspected", ET_Ignore, Param_Cell, Param_Cell, Param_String, Param_String); +} + +void Call_OnPlayerSuspected(int client, ACReason reason, const char[] notes, const char[] stats) +{ + Call_StartForward(H_OnPlayerSuspected); + Call_PushCell(client); + Call_PushCell(reason); + Call_PushString(notes); + Call_PushString(stats); + Call_Finish(); +} + + + +// =====[ NATIVES ]===== + +void CreateNatives() +{ + CreateNative("GOKZ_AC_GetSampleSize", Native_GetSampleSize); + CreateNative("GOKZ_AC_GetHitPerf", Native_GetHitPerf); + CreateNative("GOKZ_AC_GetPerfCount", Native_GetPerfCount); + CreateNative("GOKZ_AC_GetPerfRatio", Native_GetPerfRatio); + CreateNative("GOKZ_AC_GetJumpInputs", Native_GetJumpInputs); + CreateNative("GOKZ_AC_GetAverageJumpInputs", Native_GetAverageJumpInputs); + CreateNative("GOKZ_AC_GetPreJumpInputs", Native_GetPreJumpInputs); + CreateNative("GOKZ_AC_GetPostJumpInputs", Native_GetPostJumpInputs); +} + +public int Native_GetSampleSize(Handle plugin, int numParams) +{ + int client = GetNativeCell(1); + return IntMin(gI_BhopCount[client], AC_MAX_BHOP_SAMPLES); +} + +public int Native_GetHitPerf(Handle plugin, int numParams) +{ + int client = GetNativeCell(1); + int sampleSize = IntMin(GOKZ_AC_GetSampleSize(client), GetNativeCell(3)); + + if (sampleSize == 0) + { + return 0; + } + + bool[] perfs = new bool[sampleSize]; + SortByRecent(gB_BhopHitPerf[client], AC_MAX_BHOP_SAMPLES, perfs, sampleSize, gI_BhopIndex[client]); + SetNativeArray(2, perfs, sampleSize); + return sampleSize; +} + +public int Native_GetPerfCount(Handle plugin, int numParams) +{ + int client = GetNativeCell(1); + int sampleSize = IntMin(GOKZ_AC_GetSampleSize(client), GetNativeCell(2)); + + if (sampleSize == 0) + { + return 0; + } + + bool[] perfs = new bool[sampleSize]; + GOKZ_AC_GetHitPerf(client, perfs, sampleSize); + + int perfCount = 0; + for (int i = 0; i < sampleSize; i++) + { + if (perfs[i]) + { + perfCount++; + } + } + return perfCount; +} + +public int Native_GetPerfRatio(Handle plugin, int numParams) +{ + int client = GetNativeCell(1); + int sampleSize = IntMin(GOKZ_AC_GetSampleSize(client), GetNativeCell(2)); + + if (sampleSize == 0) + { + return view_as<int>(0.0); + } + + int perfCount = GOKZ_AC_GetPerfCount(client, sampleSize); + return view_as<int>(float(perfCount) / float(sampleSize)); +} + +public int Native_GetJumpInputs(Handle plugin, int numParams) +{ + int client = GetNativeCell(1); + int sampleSize = IntMin(GOKZ_AC_GetSampleSize(client), GetNativeCell(3)); + + if (sampleSize == 0) + { + return 0; + } + + int[] preJumpInputs = new int[sampleSize]; + SortByRecent(gI_BhopPreJumpInputs[client], AC_MAX_BHOP_SAMPLES, preJumpInputs, sampleSize, gI_BhopIndex[client]); + int[] postJumpInputs = new int[sampleSize]; + SortByRecent(gI_BhopPostJumpInputs[client], AC_MAX_BHOP_SAMPLES, postJumpInputs, sampleSize, gI_BhopIndex[client]); + + int[] jumpInputs = new int[sampleSize]; + for (int i = 0; i < sampleSize; i++) + { + jumpInputs[i] = preJumpInputs[i] + postJumpInputs[i]; + } + + SetNativeArray(2, jumpInputs, sampleSize); + return sampleSize; +} + +public int Native_GetAverageJumpInputs(Handle plugin, int numParams) +{ + int client = GetNativeCell(1); + int sampleSize = IntMin(GOKZ_AC_GetSampleSize(client), GetNativeCell(2)); + + if (sampleSize == 0) + { + return view_as<int>(0.0); + } + + int[] jumpInputs = new int[sampleSize]; + GOKZ_AC_GetJumpInputs(client, jumpInputs, sampleSize); + + int jumpInputCount = 0; + for (int i = 0; i < sampleSize; i++) + { + jumpInputCount += jumpInputs[i]; + } + return view_as<int>(float(jumpInputCount) / float(sampleSize)); +} + +public int Native_GetPreJumpInputs(Handle plugin, int numParams) +{ + int client = GetNativeCell(1); + int sampleSize = IntMin(GOKZ_AC_GetSampleSize(client), GetNativeCell(3)); + + if (sampleSize == 0) + { + return 0; + } + + int[] preJumpInputs = new int[sampleSize]; + SortByRecent(gI_BhopPreJumpInputs[client], AC_MAX_BHOP_SAMPLES, preJumpInputs, sampleSize, gI_BhopIndex[client]); + SetNativeArray(2, preJumpInputs, sampleSize); + return sampleSize; +} + +public int Native_GetPostJumpInputs(Handle plugin, int numParams) +{ + int client = GetNativeCell(1); + int sampleSize = IntMin(GOKZ_AC_GetSampleSize(client), GetNativeCell(3)); + + if (sampleSize == 0) + { + return 0; + } + + int[] postJumpInputs = new int[sampleSize]; + SortByRecent(gI_BhopPostJumpInputs[client], AC_MAX_BHOP_SAMPLES, postJumpInputs, sampleSize, gI_BhopIndex[client]); + SetNativeArray(2, postJumpInputs, sampleSize); + return sampleSize; +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-anticheat/bhop_tracking.sp b/sourcemod/scripting/gokz-anticheat/bhop_tracking.sp new file mode 100644 index 0000000..5607b07 --- /dev/null +++ b/sourcemod/scripting/gokz-anticheat/bhop_tracking.sp @@ -0,0 +1,336 @@ +/* + Track player's jump inputs and whether they hit perfect + bunnyhops for a number of their recent bunnyhops. +*/ + + + +// =====[ PUBLIC ]===== + +void PrintBhopCheckToChat(int client, int target) +{ + GOKZ_PrintToChat(client, true, + "{lime}%N {grey}[{lime}%d%%%% {grey}%t | {lime}%.2f {grey}%t]", + target, + RoundFloat(GOKZ_AC_GetPerfRatio(target, 20) * 100.0), + "Perfs", + GOKZ_AC_GetAverageJumpInputs(target, 20), + "Average"); + GOKZ_PrintToChat(client, false, + " {grey}%t - %s", + "Pattern", + GenerateScrollPattern(target, 20)); +} + +void PrintBhopCheckToConsole(int client, int target) +{ + PrintToConsole(client, + "%N [%d%% %t | %.2f %t]\n %t - %s", + target, + RoundFloat(GOKZ_AC_GetPerfRatio(target, 20) * 100.0), + "Perfs", + GOKZ_AC_GetAverageJumpInputs(target, 20), + "Average", + "Pattern", + GenerateScrollPattern(target, 20, false)); +} + +// Generate 'scroll pattern' +char[] GenerateScrollPattern(int client, int sampleSize = AC_MAX_BHOP_SAMPLES, bool colours = true) +{ + char report[512]; + int maxIndex = IntMin(gI_BhopCount[client], sampleSize); + bool[] perfs = new bool[maxIndex]; + GOKZ_AC_GetHitPerf(client, perfs, maxIndex); + int[] jumpInputs = new int[maxIndex]; + GOKZ_AC_GetJumpInputs(client, jumpInputs, maxIndex); + + for (int i = 0; i < maxIndex; i++) + { + if (colours) + { + Format(report, sizeof(report), "%s%s%d ", + report, + perfs[i] ? "{green}" : "{default}", + jumpInputs[i]); + } + else + { + Format(report, sizeof(report), "%s%d%s ", + report, + jumpInputs[i], + perfs[i] ? "*" : ""); + } + } + + TrimString(report); + + return report; +} + +// Generate 'scroll pattern' report showing pre and post inputs instead +char[] GenerateScrollPatternEx(int client, int sampleSize = AC_MAX_BHOP_SAMPLES) +{ + char report[512]; + int maxIndex = IntMin(gI_BhopCount[client], sampleSize); + bool[] perfs = new bool[maxIndex]; + GOKZ_AC_GetHitPerf(client, perfs, maxIndex); + int[] jumpInputs = new int[maxIndex]; + GOKZ_AC_GetJumpInputs(client, jumpInputs, maxIndex); + int[] preJumpInputs = new int[maxIndex]; + GOKZ_AC_GetPreJumpInputs(client, preJumpInputs, maxIndex); + int[] postJumpInputs = new int[maxIndex]; + GOKZ_AC_GetPostJumpInputs(client, postJumpInputs, maxIndex); + + for (int i = 0; i < maxIndex; i++) + { + Format(report, sizeof(report), "%s(%d%s%d)", + report, + preJumpInputs[i], + perfs[i] ? "*" : " ", + postJumpInputs[i]); + } + + TrimString(report); + + return report; +} + + + +// =====[ EVENTS ]===== + +void OnClientPutInServer_BhopTracking(int client) +{ + ResetBhopStats(client); +} + +void OnPlayerRunCmdPost_BhopTracking(int client, int buttons, int cmdnum) +{ + if (gCV_sv_autobunnyhopping.BoolValue) + { + return; + } + + int nextIndex = NextIndex(gI_BhopIndex[client], AC_MAX_BHOP_SAMPLES); + + // Record buttons BEFORE checking for bhop + RecordButtons(client, buttons); + + // If bhop was last tick, then record the pre bhop inputs. + // Require two times the button sample size since the last + // takeoff to avoid pre and post bhop input overlap. + if (HitBhop(client, cmdnum) + && cmdnum >= gI_BhopLastTakeoffCmdnum[client] + AC_MAX_BUTTON_SAMPLES * 2 + && gB_LastLandingWasValid[client]) + { + gB_BhopHitPerf[client][nextIndex] = Movement_GetHitPerf(client); + gI_BhopPreJumpInputs[client][nextIndex] = CountJumpInputs(client); + gI_BhopLastRecordedBhopCmdnum[client] = cmdnum; + gB_BhopPostJumpInputsPending[client] = true; + gB_BindExceptionPending[client] = false; + gB_BindExceptionPostPending[client] = false; + } + + // Bind exception + if (gB_BindExceptionPending[client] && cmdnum > Movement_GetLandingCmdNum(client) + AC_MAX_BHOP_GROUND_TICKS) + { + gB_BhopHitPerf[client][nextIndex] = false; + gI_BhopPreJumpInputs[client][nextIndex] = -1; // Special value for binded jumps + gI_BhopLastRecordedBhopCmdnum[client] = cmdnum; + gB_BhopPostJumpInputsPending[client] = true; + gB_BindExceptionPending[client] = false; + gB_BindExceptionPostPending[client] = true; + } + + // Record post bhop inputs once enough ticks have passed + if (gB_BhopPostJumpInputsPending[client] && cmdnum == gI_BhopLastRecordedBhopCmdnum[client] + AC_MAX_BUTTON_SAMPLES) + { + gI_BhopPostJumpInputs[client][nextIndex] = CountJumpInputs(client); + gB_BhopPostJumpInputsPending[client] = false; + gI_BhopIndex[client] = nextIndex; + gI_BhopCount[client]++; + CheckForBhopMacro(client); + gB_BindExceptionPostPending[client] = false; + } + + // Record last jump takeoff time + if (JustJumped(client, cmdnum)) + { + gI_BhopLastTakeoffCmdnum[client] = cmdnum; + gB_BindExceptionPending[client] = false; + if (gB_BindExceptionPostPending[client]) + { + gB_BhopPostJumpInputsPending[client] = false; + gB_BindExceptionPostPending[client] = false; + } + } + + if (JustLanded(client, cmdnum)) + { + // These conditions exist to reduce false positives. + + // Telehopping is when the player bunnyhops out of a teleport that has a + // destination very close to the ground. This will, more than usual, + // result in a perfect bunnyhop. This is alleviated by checking if the + // player's origin was affected by a teleport last tick. + + // When a player is pressing up against a slope but not ascending it (e.g. + // palm trees on kz_adv_cursedjourney), they will switch between on ground + // and off ground frequently, which means that if they manage to jump, the + // jump will be recorded as a perfect bunnyhop. To ignore this, we check + // the jump is more than 1 tick duration. + + gB_LastLandingWasValid[client] = cmdnum - gI_LastOriginTeleportCmdNum[client] > 1 + && cmdnum - Movement_GetTakeoffCmdNum(client) > 1; + + // You can still crouch-bind VNL jumps and some people just don't know that + // it doesn't work with the other modes in GOKZ. This can cause false positives + // if the player uses the bind for bhops and mostly presses it too early or + // exactly on time rather than too late. This is supposed to reduce those by + // detecting jumps where you don't get a bhop and have exactly one jump input + // before landing and none after landing. We require the one input to be right + // before the jump to make it a lot harder to fake a binded jump when doing + // a regular longjump. + gB_BindExceptionPending[client] = (CountJumpInputs(client, AC_BINDEXCEPTION_SAMPLES) == 1 && CountJumpInputs(client, AC_MAX_BUTTON_SAMPLES) == 1); + gB_BindExceptionPostPending[client] = false; + } +} + + + +// =====[ PRIVATE ]===== + +static void CheckForBhopMacro(int client) +{ + if (GOKZ_AC_GetPerfCount(client, 19) == 19) + { + SuspectPlayer(client, ACReason_BhopHack, "High perf ratio", GenerateBhopBanStats(client, 19)); + } + else if (GOKZ_AC_GetPerfCount(client, 30) >= 28) + { + SuspectPlayer(client, ACReason_BhopHack, "High perf ratio", GenerateBhopBanStats(client, 30)); + } + else if (GOKZ_AC_GetPerfCount(client, 20) >= 16 && GOKZ_AC_GetAverageJumpInputs(client, 20) <= 2.0 + EPSILON) + { + SuspectPlayer(client, ACReason_BhopHack, "1's or 2's scroll pattern", GenerateBhopBanStats(client, 20)); + } + else if (gI_BhopCount[client] >= 20 && GOKZ_AC_GetPerfCount(client, 20) >= 8 + && GOKZ_AC_GetAverageJumpInputs(client, 20) >= 19.0 - EPSILON) + { + SuspectPlayer(client, ACReason_BhopMacro, "High scroll pattern", GenerateBhopBanStats(client, 20)); + } + else if (GOKZ_AC_GetPerfCount(client, 30) >= 10 && CheckForRepeatingJumpInputsCount(client, 25, 30) >= 14) + { + SuspectPlayer(client, ACReason_BhopMacro, "Repeating scroll pattern", GenerateBhopBanStats(client, 30)); + } +} + +static char[] GenerateBhopBanStats(int client, int sampleSize) +{ + char stats[512]; + FormatEx(stats, sizeof(stats), + "Perfs: %d/%d, Average: %.2f, Scroll pattern: %s", + GOKZ_AC_GetPerfCount(client, sampleSize), + IntMin(gI_BhopCount[client], sampleSize), + GOKZ_AC_GetAverageJumpInputs(client, sampleSize), + GenerateScrollPatternEx(client, sampleSize)); + return stats; +} + +/** + * Returns -1, or the repeating input count if there if there is + * an input count that repeats for more than the provided ratio. + * + * @param client Client index. + * @param threshold Minimum frequency to be considered 'repeating'. + * @param sampleSize Maximum recent bhop samples to include in calculation. + * @return The repeating input, or else -1. + */ +static int CheckForRepeatingJumpInputsCount(int client, int threshold, int sampleSize = AC_MAX_BHOP_SAMPLES) +{ + int maxIndex = IntMin(gI_BhopCount[client], sampleSize); + int[] jumpInputs = new int[maxIndex]; + GOKZ_AC_GetJumpInputs(client, jumpInputs, maxIndex); + int maxJumpInputs = AC_MAX_BUTTON_SAMPLES + 1; + int[] jumpInputsFrequency = new int[maxJumpInputs]; + + // Count up all the in jump patterns + for (int i = 0; i < maxIndex; i++) + { + // -1 is a binded jump, those are excluded + if (jumpInputs[i] != -1) + { + jumpInputsFrequency[jumpInputs[i]]++; + } + } + + // Returns i if the given number of the sample size has the same jump input count + for (int i = 1; i < maxJumpInputs; i++) + { + if (jumpInputsFrequency[i] >= threshold) + { + return i; + } + } + + return -1; // -1 if no repeating jump input found +} + +// Reset the tracked bhop stats of the client +static void ResetBhopStats(int client) +{ + gI_ButtonCount[client] = 0; + gI_ButtonsIndex[client] = 0; + gI_BhopCount[client] = 0; + gI_BhopIndex[client] = 0; + gI_BhopLastTakeoffCmdnum[client] = 0; + gI_BhopLastRecordedBhopCmdnum[client] = 0; + gB_BhopPostJumpInputsPending[client] = false; + gB_LastLandingWasValid[client] = false; + gB_BindExceptionPending[client] = false; + gB_BindExceptionPostPending[client] = false; +} + +// Returns true if ther was a jump last tick and was within a number of ticks after landing +static bool HitBhop(int client, int cmdnum) +{ + return JustJumped(client, cmdnum) && Movement_GetTakeoffCmdNum(client) - Movement_GetLandingCmdNum(client) <= AC_MAX_BHOP_GROUND_TICKS; +} + +static bool JustJumped(int client, int cmdnum) +{ + return Movement_GetJumped(client) && Movement_GetTakeoffCmdNum(client) == cmdnum; +} + +static bool JustLanded(int client, int cmdnum) +{ + return Movement_GetLandingCmdNum(client) == cmdnum; +} + +// Records current button inputs +static void RecordButtons(int client, int buttons) +{ + gI_ButtonsIndex[client] = NextIndex(gI_ButtonsIndex[client], AC_MAX_BUTTON_SAMPLES); + gI_Buttons[client][gI_ButtonsIndex[client]] = buttons; + gI_ButtonCount[client]++; +} + +// Counts the number of times buttons went from !IN_JUMP to IN_JUMP +static int CountJumpInputs(int client, int sampleSize = AC_MAX_BUTTON_SAMPLES) +{ + int[] recentButtons = new int[sampleSize]; + SortByRecent(gI_Buttons[client], AC_MAX_BUTTON_SAMPLES, recentButtons, sampleSize, gI_ButtonsIndex[client]); + int maxIndex = IntMin(gI_ButtonCount[client], sampleSize); + int jumps = 0; + + for (int i = 0; i < maxIndex - 1; i++) + { + // If buttons went from !IN_JUMP to IN_JUMP + if (!(recentButtons[i + 1] & IN_JUMP) && recentButtons[i] & IN_JUMP) + { + jumps++; + } + } + return jumps; +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-anticheat/commands.sp b/sourcemod/scripting/gokz-anticheat/commands.sp new file mode 100644 index 0000000..a1fbe2e --- /dev/null +++ b/sourcemod/scripting/gokz-anticheat/commands.sp @@ -0,0 +1,76 @@ +void RegisterCommands() +{ + RegAdminCmd("sm_bhopcheck", CommandBhopCheck, ADMFLAG_ROOT, "[KZ] Show bunnyhop stats report including perf ratio and scroll pattern."); +} + +public Action CommandBhopCheck(int client, int args) +{ + if (args == 0) + { + if (GOKZ_AC_GetSampleSize(client) == 0) + { + GOKZ_PrintToChat(client, true, "%t", "Not Enough Bhops (Self)"); + } + else + { + PrintBhopCheckToChat(client, client); + } + return Plugin_Handled; + } + + char arg[65]; + GetCmdArg(1, arg, sizeof(arg)); + char targetName[MAX_TARGET_LENGTH]; + int targetList[MAXPLAYERS], targetCount; + bool tnIsML; + + if ((targetCount = ProcessTargetString( + arg, + client, + targetList, + MAXPLAYERS, + COMMAND_FILTER_NO_IMMUNITY | COMMAND_FILTER_NO_BOTS, + targetName, + sizeof(targetName), + tnIsML)) <= 0) + { + ReplyToTargetError(client, targetCount); + return Plugin_Handled; + } + + if (targetCount >= 2) + { + GOKZ_PrintToChat(client, true, "%t", "See Console"); + for (int i = 0; i < targetCount; i++) + { + if (GOKZ_AC_GetSampleSize(targetList[i]) == 0) + { + PrintToConsole(client, "%t", "Not Enough Bhops (Console)", targetList[i]); + } + else + { + PrintBhopCheckToConsole(client, targetList[i]); + } + } + } + else + { + if (GOKZ_AC_GetSampleSize(targetList[0]) == 0) + { + if (targetList[0] == client) + { + GOKZ_PrintToChat(client, true, "%t", "Not Enough Bhops (Self)"); + } + else + { + GOKZ_PrintToChat(client, true, "%t", "Not Enough Bhops", targetList[0]); + } + } + else + { + PrintBhopCheckToChat(client, targetList[0]); + } + } + + return Plugin_Handled; +}
\ No newline at end of file |
