summaryrefslogtreecommitdiff
path: root/sourcemod/scripting/gokz-anticheat/bhop_tracking.sp
blob: 5607b07d287d10cca18ff84220857eb28c215a97 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
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;
}