Making a good speed hack detector

In my foray into writing an anti-cheat, I’m exploring speed hacking and how to detect/prevent it. It seems that most of the server side methods follow this general outline:

local root = Character:FindFirstChild("HumanoidRootPart")
local pos1 = root.Position
local dtime = task.wait(0.016667)
local pos2 = root.Position
local dist = (pos1 - pos2).Magnitude
if dist > (dtime * Humanoid.WalkSpeed) then
	punish player
end

Apparently, this will give a lot of false positives for lagging players. I was looking into extending the dtime by adding in the ping time, but I’m not sure how to do that. Right now I have this:

local multiplier = 1.0
local ping = player:GetNetworkPing()
local dtime = (task.wait(0.016667) + (ping * 0.995)) * multiplier

Without figuring in the ping, at 0.01667 seconds and a walk speed of 16. The most the player can travel is 0.267 studs. When I figure in the ping time of say…32ms, the allowed max distance is 0.778. However, from what I have observed, these numbers tend to be inaccurate because they exceed the threshold by 2 or 3 times. Another data point that I am using is the linear velocity that all parts have, including players. This does replicate to the server, so there’s that.

This does use data from the client (yes, I know “Don’t trust the client.”), but I also have server checks too that don’t rely on the client to send accurate information.

How would I incorporate the ping number into the formula? Also, what other checks could be made with this data?

4 Likes

never make anti exploits on the client.

you are going to get false positives for anti speed because of lag.

make a bit of room for the speed not expecting them to be at that exact speed all the time. i suggest you give at least 10 sps (studs per second)

put this in a runservice.heartbeat with a debounce (a variable that is used to delay stuff in functions that dont need to delay the entire one) debounce wait time best for anti speed is 0.1

debounce example:

local debounce = false
RunService.Heartbeat:Connect(function()
    if debounce then return end
    debounce = true

    task.wait(1)
    debounce = false
end)

if you are going to ban the player for speeding that isnt a good idea because again, lag or latency. i suggest you kick them with a reason

I know what a debounce is. I use it all the time. Beyond what I have stated, I cannot say publicly how my system works, but the server has the final word. I will say that there is a heater and a progressive punishment system in place.

1 Like

then just add a bit of room for lag, there is no such thing as a perfect anti speed.

2 Likes

As a roblox developer theres sadly not much you can do about this currently, but roblox is working on their anticheat.

In the meantime you can use a serverscript like so:

local maxtraveldist = 25
local looptime = 0.1

game:GetService('Players').PlayerAdded:Connect(function(player)
	player.CharacterAdded:Connect(function(character)
		local currentlocation = character.HumanoidRootPart.Position * Vector3.new(1,0,1)
		
		local lastlocation = character.HumanoidRootPart.Position* Vector3.new(1,0,1)
		
		spawn(function()
			while true do
				task.wait(looptime)
				currentlocation = character.HumanoidRootPart.Position * Vector3.new(1,0,1)
				local dist = (currentlocation - lastlocation).Magnitude / looptime
				if dist > maxtraveldist then
					character.HumanoidRootPart.Position = lastlocation
				else
					lastlocation = character.HumanoidRootPart.Position * Vector3.new(1,0,1)
				end
			end
		end)
	end)
end)

This solution does not take Jumppower into considartion.

I have a separate check for jump power, and mine takes the character state into consideration as well. One thing though, why multiply the position by the vector <1, 0, 1> and mask out the Y coordinate? It would seem to be needed to place the player correctly in the vertical plane.

1 Like

That’s why I was trying to incorporate the ping time into the calculation. I am trying to create a sliding lag window based on the player’s ping. The window starts at the end of the hard distance calculation.

You can also make a Y coordinate modification for this script, i just didnt do it because i was lazy and the magnitude from the last root and new root isnt the way to go about it, jumping will increase that magnitude alot. Sorry i didnt make the clear.

1 Like

Thats easily bypassable by the way.

If a player falls you’d detect a “speed hack”, making a reliable walkspeed detector is pretty hard especially for versatile games where stuff like cars, teleports and multiple things that can change the player’s velocity. What @O3_O2 posted will work well after some modification, but I recommend modularizing and improving it (It’s not the most efficient nor best code) so if you needed to temporarily whitelist a player, let’s say if they got launched by a rocket or something. But do note that when you whitelist someone they could do a teleport of some sort without detection. I also don’t think ping is something worth accounting for (and I don’t even think that method is very reliable- AFAIK players can mask their ping / force a high ping during a teleport to bypass your system), players don’t move at different velocities if they’re lagging unless they spike, in which case they would feel a jitter but if it’s a spike it’ll only be one jitter and it’d be fine.

I agree, what I posted was totally just concept for him to work with.

Quickly made a module, it’s not perfect but it works.

local SpeedhackDetector = { tracking = {}, whitelist = {}, lastCheck = tick() }

local XZ = Vector3.new(1, 0, 1)

function SpeedhackDetector.Add(character)
	SpeedhackDetector.tracking[character] = character.PrimaryPart.CFrame
	
	character.Destroying:Connect(function()
		SpeedhackDetector.tracking[character] = nil
	end)
end

function SpeedhackDetector.WhitelistCharacter(character, duration)
	local tag = tick()

	SpeedhackDetector.whitelist[character] = tag
	
	task.delay(duration, function()
		if SpeedhackDetector.whitelist[character] == tag then
			SpeedhackDetector.whitelist[character] = nil
		end
	end)
end

function SpeedhackDetector.BlacklistCharacter(character)
	SpeedhackDetector.whitelist[character] = nil
end

function SpeedhackDetector.Update()
	for character, lastCFrame in SpeedhackDetector.tracking do
		local humanoid = character:FindFirstChild("Humanoid")
		
		if not humanoid or SpeedhackDetector.whitelist[character] then
			continue
		end
		
		local distancePerSecond = humanoid.WalkSpeed * 1.2
		local maxDistance = (tick() - SpeedhackDetector.lastCheck) * distancePerSecond
		
		local currentCFrame = character.PrimaryPart.CFrame
		
		local compLast = lastCFrame.Position * XZ
		local compCurrent = currentCFrame.Position * XZ
		
		if (compLast - compCurrent).Magnitude > maxDistance then
			print("Player exceeded max distance")
			character:PivotTo(lastCFrame)
			SpeedhackDetector.tracking[character] = lastCFrame
		else
			SpeedhackDetector.tracking[character] = currentCFrame
		end
	end
	
	SpeedhackDetector.lastCheck = tick()
end

return SpeedhackDetector

Accommodating server script:

local SpeedhackDetector = require(script.SpeedhackDetector)

local function CharacterAdded(character)
	SpeedhackDetector.Add(character)
end

game:GetService("Players").PlayerAdded:Connect(function(player)
	CharacterAdded(player.Character or player.CharacterAdded:Wait())
	player.CharacterAdded:Connect(CharacterAdded)
end)

local t = 0
game:GetService("RunService").Heartbeat:Connect(function(dt)
	t += dt
	
	if t >= 0.5 then
		t = 0
		SpeedhackDetector.Update()
	end
end)

Here’s an example of how it’s not perfect:
https://gyazo.com/b2b31137bc03334c246f67d3b56c699f

@O3_O2, @malakopter Interesting. I have come up with some code. The detector only works if the humanoid state is running, climbing, or swimming. So jumping or falling won’t trigger it. Furthermore, if it does trigger, it looks at the Y coordinate. Looking at my original post, if pos1.Y > pos2.Y, then it won’t trigger for that either. In my testing, it seems that linear velocity can go above the walkspeed a little bit, so math.floor() is used to drop the fractional part.

My game does have teleport and jump pads. A jump pad applies a vector force to the character, but I don’t know what state they are in. A teleport, will move the character’s position to a different location. I have provisions built into the system to disable it for a very short period while the jump/teleport takes place. Checking two different data points instead of one seems to yield more accurate results.

You should not disable it, instead have a variable inside the player that tells the server how fast the player is allowed to go.

Like so:

local looptime = 0.05

game:GetService('Players').PlayerAdded:Connect(function(player)
	local maxtraveldist = Instance.new('NumberValue',player)
	maxtraveldist.Value = 25
	maxtraveldist.Name = 'MaxTravelDist'
	
	player.CharacterAdded:Connect(function(character)
		local currentlocation = character.HumanoidRootPart.Position * Vector3.new(1,0,1)

		local lastlocation = character.HumanoidRootPart.Position* Vector3.new(1,0,1)

		spawn(function()
			while true do
				
				task.wait(looptime)
				currentlocation = character.HumanoidRootPart.Position * Vector3.new(1,0,1)
				local dist = (currentlocation - lastlocation).Magnitude / looptime
				if dist > maxtraveldist.Value then
					character.HumanoidRootPart.Position = lastlocation
				else
					lastlocation = character.HumanoidRootPart.Position * Vector3.new(1,0,1)
				end
			end
		end)
	end)
end)

Mainly because, if you disable it for just a split second, the exploiter can teleport across the map.

I wouldn’t recommend that. Exploiters are given full authority over their character- including what state their Humanoid is in. They could very easily make it so they’re falling whilst they’re moving really quickly as I’ve seen exploiters do in games like Swordburst 2, where exploiters automatically go around the map hitting every mob. So if we did check every state, it would make checking the Y component obsolete and thus your flooring obsolete. @O3_O2 has a good idea about instead of completely disabling it, just making the max distance travelled every interval bigger to match your jump pads or whatever you need.

The problem is the calculation for that. Gravity, player mass, and force will all have to be calculated to come up with a number. It will vary between players. I don’t think there would be an issue with the exploiter teleporting across the map. Yes, I can probably set the maximum distance they can travel during the teleport/jump.

@malakopter On the client, but what about the server? Can they force the server to think their client is in a particular state? The checks are done on both sets of data.

1 Like

Absolutely! As I said, full authority. They can destroy their limbs, hats, change their humanoid’s state, force network ownership of unanchored parts; a bunch of stuff, unfortunately. I mean I hope this all gets sorted out but it’s kind of necessary for a smooth experience on every experience.

I have a workaround idea for that. If the server knows that the player has a speed boost, the speed anticheat will be more lenient.

You know what, I’ve done some testing and I’ve actually made it pretty alright.
Now this was fully tested in Studio, I highly recommend you test it yourself in your own game but even when changing the player’s state on the client (yes, it does replicate - I tested to make sure) and the server gives lenience of 100 studs per second (I check every .4 seconds so it translates to 40 studs on the Y axis every interval if the player is falling).
I added a Speed Gain feature to the module and tested with a +100 stud conveyor, and it works really really well!

So this is the new module for it.

local SpeedhackDetector = { tracking = {}, whitelist = {}, additionalSpeeds = {tags = {}, speeds = {}}, lastCheck = tick() }

local XZ = Vector3.new(1, 0, 1)
local Y = Vector3.yAxis

function SpeedhackDetector.Add(character)
	SpeedhackDetector.tracking[character] = character.PrimaryPart.CFrame
	
	character.Destroying:Connect(function()
		SpeedhackDetector.tracking[character] = nil
	end)
end

function SpeedhackDetector.WhitelistCharacter(character, duration)
	local tag = tick()

	SpeedhackDetector.whitelist[character] = tag
	
	task.delay(duration, function()
		if SpeedhackDetector.whitelist[character] == tag then
			SpeedhackDetector.whitelist[character] = nil
		end
	end)
end

function SpeedhackDetector.BlacklistCharacter(character)
	SpeedhackDetector.whitelist[character] = nil
end

function SpeedhackDetector.ChangeSpeedGain(character, speedGain)
	SpeedhackDetector.additionalSpeeds.speeds[character] = speedGain
end

function SpeedhackDetector.IncreaseSpeedGain(character, tag, speedGain)
	if not SpeedhackDetector.additionalSpeeds.speeds[character] then
		SpeedhackDetector.additionalSpeeds.speeds[character] = 0
		SpeedhackDetector.additionalSpeeds.tags[character] = {}
	end
	
	if SpeedhackDetector.additionalSpeeds.tags[character][tag] then
		return
	end
	
	SpeedhackDetector.additionalSpeeds.tags[character][tag] = true
	SpeedhackDetector.additionalSpeeds.speeds[character] += speedGain
end

function SpeedhackDetector.DecreaseSpeedGain(character, tag, speedGain)
	local current = SpeedhackDetector.additionalSpeeds[character] or 0
	
	SpeedhackDetector.additionalSpeeds.tags[character][tag] = nil
	SpeedhackDetector.additionalSpeeds.speeds[character] = math.max(current - speedGain, 0)
end

function SpeedhackDetector.Update()
	for character, lastCFrame in SpeedhackDetector.tracking do
		local humanoid: Humanoid = character:FindFirstChild("Humanoid")
		
		if not humanoid or SpeedhackDetector.whitelist[character] then
			continue
		end
		
		--// Did a test, terminal velocity of jumping is roughly ~1.063*jumpPower.
		--// I tested on different masses, gravities, jump powers, etc. All gave this result.
		local distancePerSecond = humanoid.WalkSpeed * 1.1
		local jumpDistancePerSecond = humanoid.JumpPower * 1.063 * 1.1
		
		if humanoid:GetStateEnabled(Enum.HumanoidStateType.Freefall) then
			jumpDistancePerSecond = 100
		end
		
		local speedGain = SpeedhackDetector.additionalSpeeds.speeds[character]

		if speedGain then
			distancePerSecond += speedGain
			jumpDistancePerSecond += speedGain
		end
		
		local maxDistance = (tick() - SpeedhackDetector.lastCheck) * distancePerSecond
		local maxDistanceY = (tick() - SpeedhackDetector.lastCheck) * jumpDistancePerSecond
				
		local currentCFrame = character.PrimaryPart.CFrame
		
		local compLast = lastCFrame.Position * XZ
		local compCurrent = currentCFrame.Position * XZ
		
		local compLastY = lastCFrame.Position * Y
		local compCurrentY = currentCFrame.Position * Y
		
		if (compLast - compCurrent).Magnitude > maxDistance or (compLastY - compCurrentY).Magnitude > maxDistanceY then
			print("Player exceeded max distance")
			character:PivotTo(lastCFrame)
			SpeedhackDetector.tracking[character] = lastCFrame
		else
			SpeedhackDetector.tracking[character] = currentCFrame
		end
	end
	
	SpeedhackDetector.lastCheck = tick()
end

return SpeedhackDetector

And I added a folder that contains items that will speed you up by touching it, but the API supports anything.

local function InitializeSpeeder(speedPart)
	local gain = speedPart:GetAttribute("SpeedGain")
	
	speedPart.Touched:Connect(function(h)
		local c = h:FindFirstAncestorOfClass("Model")
		
		if c and c:FindFirstChild("Humanoid") then
			SpeedhackDetector.IncreaseSpeedGain(c, speedPart, gain)
		end
	end)
	
	speedPart.TouchEnded:Connect(function(h)
		local c = h:FindFirstAncestorOfClass("Model")

		if c and c:FindFirstChild("Humanoid") then
			task.delay(1, SpeedhackDetector.DecreaseSpeedGain, c, speedPart, gain)
		end
	end)
end

for _, speeder in workspace.SpeederUppers:GetChildren() do
	InitializeSpeeder(speeder)
end

We can walk on the conveyor
https://gyazo.com/036d1cf6e741b772ae058c0d64d7a046

But not speed hack!
https://gyazo.com/fa8f3d14aa2317e81f324a497f8c804c

This is still not perfect though, exploiters can use around up to 110 Jump Power before being detected.
And this doesn’t address the velocity you get when jumping off a Truss.

Simulating 400 ping- it works well :slight_smile:
https://gyazo.com/ae185df197f3e15683c3bed28ba61e35

1 Like