Linear Regression Movement Anti-Exploit

DISCLAIMER: I’m just a stats student so if I get some of my terminology or explanations wrong then don’t get too mad.

This is a serverside movement anti-exploit that I have been working on for the past few days. If you haven’t taken a statistics course then you might not know what a “linear regression model” means. It’s a line that is used to predict where data should land on a graph. It is also known as the line of best fit.
Example:
400px-Linear_regression.svg

In the context of this script, it takes the ping of the player and decides the maximum reasonable (with a lot of leeway) amount of studs you should be able to move with your amount of ping. If you move above what is reasonable for your ping, then you are teleported back. This script also automatically collects data to make itself more accurate over time. You can check out the game where it is running here: Linear regression movement anti-exploit - Roblox
Here is the main server script:

local players = game:GetService("Players")
local replicatedStorage = game:GetService("ReplicatedStorage")
local httpService = game:GetService("HttpService")
local remote = Instance.new("RemoteEvent")
remote.Name = "Check"
remote.Parent = replicatedStorage
local tempdata = {}
local nextping = {} --ping GUIDs
local lastmove = {} --last movement per player
local lastping = {} --last ping delay
local startping = {} --time tracker for ping function

local data = {}

local response1 =
	httpService:RequestAsync( -- This grabs the data from my sql server
		{
			Url = "https://kiwiscripts.xyz/censored",
			Method = "GET"
		}
	)
local function tableLen(input)
	local length = 0
	for i, v in pairs(input) do
		length = length + 1
	end
	return length
end
data = httpService:JSONDecode(response1.Body) -- data is formatted like data[1]["ping"] data[1]["dist"] fyi

_G.waitTime = 1
_G.remoteWait = 1
_G.lscore = 1.5
-- all calculations will be done with population formulas because we have the whole data set
-- take all of my terminology and math with a grain of salt, because I'm just a stats student
--average of x (ping) and y (dist)
local totalx = 0
local totaly = 0
for i, v in pairs(data) do
	totalx = totalx + v["ping"]
	totaly = totaly + v["dist"]
end
local avgy = (totaly / tableLen(data))
local avgx = (totalx / tableLen(data))
--standard deviation
local presx = 0
local presy = 0
for i, v in pairs(data) do
	presx = presx + ((v["ping"] - avgx) ^ 2)
	presy = presy + ((v["dist"] - avgy) ^ 2)
end
local stanx = math.sqrt(presx / tableLen(data))
local stany = math.sqrt(presy / tableLen(data))
--calculate r
local prer = 0
for i, v in pairs(data) do
	prer = prer + ((v["ping"] - avgx) * (v["dist"] - avgy)) --didn't feel like storing this from standard deviation calculation :shrug:
end
local r = (prer / (stanx * stany)) / tableLen(data)
--   finally, making the equation  --
statslope = r * (stany / stanx)
--finding a
stata = (-(statslope * avgx)) + avgy
--getting standard deviation of residual
local presr = 0
for i, v in pairs(data) do
	presr = presr + ((v["dist"] - (stata + (v["ping"] * statslope))) ^ 2)
end
stanres = math.sqrt(presr / tableLen(data))
-- it is done...
local function checkStat(dist, ping) --calculate score using residual and predicted value instead of standard distribution in denominator and average
	return (dist - (stata + (ping * statslope))) / stanres
end

local function main(player)
	local pid = tostring(player.UserId)
	tempdata[pid] = {}
	local charadded =
		player.CharacterAdded:Connect(
			function(character)
			wait()
			while true do
				local humroot = character:WaitForChild("HumanoidRootPart")
				if humroot then
					lastmove[pid] = Vector3.new(humroot.Position.X, 0, humroot.Position.Z) -- doesn't measure y axis movement b/c who cares if exploiters can teleport 500 feet in the air
					local actpos = humroot.Position
					wait(_G.waitTime)
					local waited = 0
					while not lastping[pid] do --wait unitl the client responds for the first time
						waited = waited + 1
						if waited >= 160 then
							player:Kick("Client timed out")
						end
						wait()
					end

					local distmove = (Vector3.new(humroot.Position.X, 0, humroot.Position.Z) - lastmove[pid]).Magnitude
					if (checkStat(distmove, lastping[pid]) > _G.lscore) or (distmove > 30) then --check if the distance moved is larger than the acceptable amount of normal residual deviations from the line + hard cutoff
						humroot.Position = actpos --using actual previous position. Don't want to tp players to y=0
					else
						table.insert(tempdata[pid], {math.round(lastping[pid]), math.round(distmove)}) --round and insert into temp data store
					end
				end
			end
		end
		)
	nextping[pid] = httpService:GenerateGUID(false)
	coroutine.wrap(
		function()
			while wait(_G.remoteWait) do
				remote:FireClient(player, nextping[pid])
				startping[pid] = tick()
			end
		end
	)()
end

for i, player in pairs(players:GetPlayers()) do --This had to be added b/c sometimes the script wouldn't load fast enough
	main(player)
end
players.PlayerAdded:Connect(
	function(player)
		main(player)
	end
)
players.PlayerRemoving:Connect(
	function(player)
		local most = -100
		local highest
		local pid = tostring(player.UserId)
		for i, v in pairs(tempdata[pid]) do
			if (checkStat(v[2], v[1]) > most) and (checkStat(v[2], v[1]) < 1) and (checkStat(v[2], v[1]) > -1) then -- Only insert reasonable data to hopefully not skew the data with people standing still or teleporting
				highest = v
				most = checkStat(v[2], v[1])
			end
		end

		if highest then
			local response =
				httpService:RequestAsync( -- This inputs the data to my sql server
					{
						Url = "https://kiwiscripts.xyz/censored",
						Method = "GET"
					}
				)
		end
	end
)
remote.OnServerEvent:Connect(
	function(player, incomingid)
		local pid = tostring(player.UserId)
		if incomingid == nextping[pid] then
			lastping[pid] = 1000 * (tick() - startping[pid]) --seconds to miliseconds
		end
		nextping[pid] = httpService:GenerateGUID(false)
	end
)

Known issues:

  • Ping can be artificially manipulated by the client - this script is more of a proof of concept anyways :man_shrugging:

  • Sometimes the movement tracking can have issues and continuously lag you back

5 Likes

This is really cool. Especially the part where you mention that it automatically collects data for accuracy, just wow.

I tried checking out the game you provided, and it’s private. Thought you should know :stuck_out_tongue:

Another thing, I’d switch over to :Connect from :connect as :connect is deprecated. Not sure if you’re looking for feedback on the code itself but yeah, thought that was another thing you should be aware of.

2 Likes

Oops, I thought I made the game public and I always thought connect was just an alias for Connect tbh. Thanks for the feedback!
Edit: Made game public and updated code

1 Like

They seem to work about the same, but :connect() and :wait() have both been deprecated in favour of :Connect/:Wait which is probably because it’s inconsistent with Roblox’s avid use of pascal case. If some big internal change were to happen (very unlikely but still), the lowercase ones could stop working/stop working consistently and it won’t be updated.

1 Like