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:
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: https://www.roblox.com/games/7547165584/Linear-regression-movement-anti-exploit
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
-
Sometimes the movement tracking can have issues and continuously lag you back