A solid Anti-Teleport System

Ghost’s Anti-Teleport System

Version 2.0

What is it?
It’s a customizable anti-teleport script. It is not bypassable by setting your character’s fall state. It allows you to toggle whether or not the system is monitoring a player. All the parameters used in the script are customizable. It can tell the difference between a large fall and a teleport. It compensates for high ping behavior. It also can be toggled to try and put a player back to their last known location x amount of times before kicking them. Plus, it even has some tuning tools. TLDR: It’s a customizable Anti-Exploit Script.

Why am I posting about it/Why did I create it?
Two reasons. One: I wanted feedback, It’s my first time creating something like this, and I’m sure I will get some feedback and improvement Ideas from others. Two: To get it out there, I couldn’t find anything that looked good to me, so I thought I’d make one for anyone to use.

How does it work?
On a user-defined interval, it checks the location of a given player and compares that to where they were before the interval. It also stores the time (os.time()) that the last check occured on. It then uses that information + (if specified) the users to decide if the person teleported. If it decides they may have teleported or if they just fell, it makes this choice by looking at the direction of the movement, and how much the change and Y contributed to that movement. It also has what I call a correction system, which when enabled (correctedViolations > 0) will attempt to move the player back to their last known valid location. It will do this a user-specified number of times before it attempts to kick them. Each time the player’s position is valid, a correction is removed from their history, meaning a false flag can be erased.

What’s new in V2?
-Game now tracks your last stable position (last time your humanoids floormaterial was something other than air)
-Correction system uses last stable position and detection system uses last know position.
-Togglable anti-fling to the correction system
-Bindable function to allow you to adjust the tolerance of detection on a per player basis
-Better handling of falling players
-A few improvements to the in script guide
-Move the connecting of the events/functions above the main loop bcuz apparently I released V1 without those working.

The code:
-The edits to this post will be me adjusting this.

local playerList = {} --Ignore this, its just a holder for player data.
local maxDistance = 40 --The max distance a player can move in a given interval
local interval = 1 --Time in seconds between position checks (I would not make this much lower than 1)
local kickMessage = "Unstable Connection Detected." --The message a player will see when they are kicked.
local correctedViolations = 3 --The game will try to teleport a player back to their last valid location this many times before kicking them.
local maxTableEntries = 100 --The max number of entries in the table before the game tries to clean it.
local scaleWithWalkSpeed = true --Controls if the anti-exploit will factor in walkspeed into its equations
local testing = true --If you want to adjust the values to fit your game, you can turn this on to get extra info from this script
local antiFling = true --Turn this off if you do not want the code to attempt to prevent flinging

--[[                             I WOULD NOT RECOMMEND MESSING WITH ANYTHING BELOW THIS MESSAGE                             ]]--


--Check that security is good.
if script:IsDescendantOf(game:GetService("ServerScriptService")) == false then warn("This script should not be placed outside of ServerScriptService, it can be put in a folder or something as long as that folder is in ServerScriptService.") script.Parent = game:GetService("ServerScriptService") end

--this is a deep copy function that also removes nil entries.
local function removeNilEntries(tab)
	local newTab = {}
	--go through table entries
	for i,v in pairs(tab) do
		--check if we need to copy or deep copy
		if typeof(v) == "table" then
			--deep copy
			newTab[i] = removeNilEntries(v)
		else
			if v ~= nil then
				--copy
				newTab[i] = v
			end
		end
	end
	--return the new cleaner table
	return newTab
end

--add player to tracking list on join
game.Players.PlayerAdded:Connect(function(plr)
	--wait for character to exist
	repeat task.wait(1) until plr.Character ~= nil and plr.Character.Parent == game.Workspace
	--create entry at players userid
	playerList[plr.UserId] = {
		--contain a player reference, their spawned position, the time this position was taken, there exempt from tping status,
		--the number of correction attempts, and the time of the last correction
		player = plr,
		LastPosition = plr.Character:WaitForChild("HumanoidRootPart").Position,
		LastTime = os.time(),
		exempt = false,
		corrections = 0,
		timeOfLastCorrection = os.time(),
		lastStablePosition = plr.Character:WaitForChild("HumanoidRootPart").Position,
		localTolerance = 1,
		lastLocalTolerance = 1,
		oldWalkSpeed = plr.Character.Humanoid.WalkSpeed,
		--create a connection that prevents dying from being detected as teleporting (respawn)
		ConnectionA = plr.Character.Humanoid.Died:Connect(function()
			playerList[plr.UserId].exempt = true
		end),
		--connection to refresh the other connection on respawn and unexempt a player from detection.
		ConnectionB = plr.CharacterAdded:Connect(function(char)
			--yeet the old connection
			playerList[plr.UserId].ConnectionA:Disconnect()
			--create a new connection that exempts a player from tp detection on death
			playerList[plr.UserId].ConnectionA = char:WaitForChild("Humanoid").Died:Connect(function() 
				playerList[plr.UserId].exempt = true
			end)
			--wait for player to be in workspace
			repeat wait(.1) until plr.Character.Parent == game.Workspace
			--Update respawned position
			playerList[plr.UserId].LastPosition = plr.Character:WaitForChild("HumanoidRootPart").Position
			--unexempt player
			playerList[plr.UserId].exempt = false
		end)
	}
end)

--stop tracking a player that leaves
game.Players.PlayerRemoving:Connect(function(plr)
	--Clean up out connections and clean up our table
	playerList[plr.UserId].ConnectionA:Disconnect()
	playerList[plr.UserId].ConnectionB:Disconnect()
	playerList[plr.UserId] = nil
end)

--Toggle function
function doToggle(player)	
	--Check that player is valid
	assert(player, "Player argument was nil.")
	assert(player.ClassName == "Player", "Player argument was "..player.ClassName.." expected Player.")
	print("Toggled")
	--Do toggle
	playerList[player.UserId].exempt = not playerList[player.UserId].exempt
	--Check if we toggled it off
	if playerList[player.UserId].exempt == false then
		--Incase we are yielding, better not error the calling code.
		local s,f = pcall(function()
			--Check if they are alive
			if player.Character ~= nil and player.Character:FindFirstChild("Humanoid") ~= nil and player.Character.Humanoid.Health > 0 then
				--Update to their current location
				playerList[player.UserId].LastPosition = player.Character.HumanoidRootPart.Position
				playerList[player.UserId].LastTime = os.time()
			end
		end)
	end

	return playerList[player.UserId].exempt
end

--update individual players max limits with this bad boi
function updateLocalTolerance(player, newTolerance)
	--We will store the old one to return later, in case someone needs to remember the old tolerance for some reason.
	local oldTolerance = playerList[player.UserId].localTolerance
	playerList[player.UserId].localTolerance = newTolerance
	return oldTolerance
end

--Handle toggle requests without yield
script:WaitForChild("Toggle").Event:Connect(function(player)
	doToggle(player)
end)

--Handle toggle requests with yield
script:WaitForChild("ToggleWithYield").OnInvoke = doToggle

--Handle tolerance requests
script:WaitForChild("Tolerance").OnInvoke = updateLocalTolerance

--main loop
while task.wait(interval) do
	--Check how big our table has gotten
	if #playerList > maxTableEntries then
		--Remove all the dead table entries
		playerList = removeNilEntries(playerList)
	end
	--go through all players
	for index, playerInfo in pairs(playerList) do
		--an uncaught error won't break the whole code
		local s,f = pcall(function()
			--check if player is allowed to tp
			if playerInfo ~= nil and playerInfo.exempt == false then
				local movementVector = playerInfo.player.Character.HumanoidRootPart.Position - playerInfo.LastPosition
				local passedTime = os.time()-playerInfo.LastTime
				--check if player moved at all (The beauty here is that this also allows us to compensate for high ping players)
				if movementVector.magnitude > 1 then
					--create a tolerance factor
					playerInfo.oldWalkSpeed = playerInfo.oldWalkSpeed > playerInfo.player.Character.Humanoid.WalkSpeed and playerInfo.oldWalkSpeed or playerInfo.player.Character.Humanoid.WalkSpeed
					local tolerance = scaleWithWalkSpeed and playerInfo.oldWalkSpeed/16 or 1
					local playerTolerance = playerInfo.lastLocalTolerance >= playerInfo.localTolerance and playerInfo.lastLocalTolerance or playerInfo.localTolerance
					playerInfo.lastLocalTolerance = playerInfo.localTolerance
					--check if the movement is sus
					if movementVector.magnitude > maxDistance*passedTime*tolerance*playerTolerance then
						--this movement was sus, but lets examine it a little closer
						--check if player was falling 
						--(I define falling as a movement in the Y/-Y direction where the 2/3 magnitude of the movement is less than the change in Y)
						if movementVector.magnitude * .66 > math.abs(movementVector.Y) then
							--we moved down/up, but most of our distance was covered by side to side motion
							--check if we need to issue a correction or yeetus the player
							if playerInfo.corrections < correctedViolations then
								--add a correction to their record
								playerInfo.corrections += 1
								--do the correction
								playerInfo.player.Character:SetPrimaryPartCFrame(CFrame.new(playerInfo.lastStablePosition))
								playerInfo.LastPosition = playerInfo.lastStablePosition
								
								if antiFling == true then
									playerInfo.player.Character.HumanoidRootPart.AssemblyLinearVelocity = Vector3.new(0,0,0)
								end
								if testing then
									warn("Violation of "..movementVector.magnitude.." by "..playerInfo.player.Name.." Max Movement: "..(maxDistance*passedTime*tolerance*playerTolerance))
								end
							else
								--remove the player
								playerInfo.player:Kick(kickMessage)
							end
						else 
							--Update info, check if testing, if so, post notif
							playerInfo.LastPosition = playerInfo.player.Character.HumanoidRootPart.Position
							playerInfo.LastTime = os.time()
							playerInfo.oldWalkSpeed = playerInfo.player.Character.Humanoid.WalkSpeed
							if testing then
								warn("A fall/rise was detected.")
							end
						end
					else
						--it was not sus, update our infomation
						playerInfo.LastPosition = playerInfo.player.Character.HumanoidRootPart.Position
						playerInfo.LastTime = os.time()
						playerInfo.oldWalkSpeed = playerInfo.player.Character.Humanoid.WalkSpeed
						--check if player is on the ground
						if playerInfo.player.Character.Humanoid.FloorMaterial ~= Enum.Material.Air then
							--update last stable position
							playerInfo.lastStablePosition = playerInfo.LastPosition
						end
						--if we are in testing mode, print out the info
						if testing then
							local tolerance = scaleWithWalkSpeed and playerInfo.oldWalkSpeed/16 or 1 
							print("No violation for "..playerInfo.player.Name.." movement was "..movementVector.magnitude.." max movement was "..(maxDistance*passedTime*tolerance))
						end
						--check if we can remove a strike from the player
						if playerInfo.corrections > 0 and os.time() - playerInfo.timeOfLastCorrection > interval then
							--we can remove it from their record so we will
							playerInfo.corrections -= 1
						end
					end
				else
					--Player is still, check how long they have been still
					if os.time() - playerInfo.LastTime > 3*interval then
						--Limit Compensation
						playerInfo.LastTime += interval
					end
				end
			end
		end)
		--check for and warn in console any errors
		if not s then warn(f) end
	end
end

The link
If you would like a copy, feel free to take one here:

In Conclusion
I look forward to reading feedback and I hope this post can help someone somewhere.

11 Likes

this works with custom characters too right?

2 Likes

If that custom character has a Humanoid and a HumanoidRootPart, it should work just fine.

2 Likes

Consider replacing this with a better snippet:

if not player.Character then
    player.CharacterAdded:Wait()
end
1 Like

There are cases where CharacterAdded fires before the event is connected, this has caused me problems in the past, which is why I picked the repeat part, appreciate the suggestion though.

2 Likes

I agree. This function is so broken, it fired before the character was instanced at all

2 Likes