I asked claude to make me an anticheat and i explained different possibilities to cheat to him but im not sure the anticheat is good and optimized
if anyone can look over it and tell me is it good enough for a game with 25 people in a server
--[[
AntiExploitModule
Server-authoritative movement anti-exploit: speed, teleport, fly, and a basic
noclip check. Fully server-side β nothing here trusts or waits on the client.
HIERARCHY (all ModuleScripts unless noted):
ServerScriptService
β Script -- local AntiExploit = require(script.AntiExploitModule); AntiExploit:Start()
β AntiExploitModule -- this file
β Config
β Utils
β FlagClass
β PlayerClass
PUBLIC API:
AntiExploit:Start()
AntiExploit:Stop()
AntiExploit:Whitelist(player, seconds)
Call this from YOUR gameplay code right before you move a player yourself
(teleport pads, launch pads, elevators, cutscenes, dash abilities) so the
anti-exploit doesn't flag your own systems as cheating.
AntiExploit.PlayerFlagged : BindableEvent(player, reasonString, countsTable)
Fires right before a kick. Hook your logging/ban systems into this event
instead of editing this module (e.g. call Restrictly.Ban from a listener).
]]
local Players = game:GetService("Players")
local Config = require(script.Config)
local PlayerClass = require(script.PlayerClass)
local AntiExploit = {}
AntiExploit.Started = false
AntiExploit.Monitoring = {}
AntiExploit.PlayerFlagged = Instance.new("BindableEvent")
function AntiExploit:AddPlayer(player)
if self.Monitoring[player] then
return
end
self.Monitoring[player] = PlayerClass.new(player)
end
function AntiExploit:RemovePlayer(player)
local obj = self.Monitoring[player]
if obj then
obj:Destroy()
self.Monitoring[player] = nil
end
end
function AntiExploit:Whitelist(player, seconds)
local obj = self.Monitoring[player]
if obj then
obj:Whitelist(seconds)
end
end
function AntiExploit:_punish(player, obj)
local parts = {}
for kind, count in pairs(obj.Flags.Counts) do
if count > 0 then
table.insert(parts, kind .. ":" .. count)
end
end
local reason = table.concat(parts, ", ")
warn(string.format("[AntiExploit] Kicking %s (%s)", player.Name, reason))
self.PlayerFlagged:Fire(player, reason, obj.Flags.Counts)
player:Kick("Removed for suspicious movement activity. Rejoin if this was a mistake. [" .. reason .. "]")
self:RemovePlayer(player)
end
function AntiExploit:Start()
if self.Started then
warn("[AntiExploit] Already started")
return
end
self.Started = true
for _, plr in ipairs(Players:GetPlayers()) do
self:AddPlayer(plr)
end
self._playerAddedConn = Players.PlayerAdded:Connect(function(plr)
self:AddPlayer(plr)
end)
self._playerRemovingConn = Players.PlayerRemoving:Connect(function(plr)
self:RemovePlayer(plr)
end)
-- One batched loop for every player instead of a connection/thread per player.
-- This is the single biggest performance win over the old system: checks run
-- CheckInterval seconds apart (plenty for catching sustained cheats) instead
-- of on every Heartbeat for every player.
task.spawn(function()
while self.Started do
local iterationStart = os.clock()
for plr, obj in pairs(self.Monitoring) do
local ok, err = pcall(obj.Update, obj)
if not ok then
warn("[AntiExploit] Update error for " .. plr.Name .. ": " .. tostring(err))
elseif obj.Flags.Score >= Config.Punishment.FlagThreshold then
self:_punish(plr, obj)
end
end
local elapsed = os.clock() - iterationStart
task.wait(math.max(0, Config.CheckInterval - elapsed))
end
end)
print("[AntiExploit] Started")
end
function AntiExploit:Stop()
if not self.Started then
warn("[AntiExploit] Already stopped")
return
end
self.Started = false
if self._playerAddedConn then
self._playerAddedConn:Disconnect()
end
if self._playerRemovingConn then
self._playerRemovingConn:Disconnect()
end
for plr in pairs(self.Monitoring) do
self:RemovePlayer(plr)
end
print("[AntiExploit] Stopped")
end
return AntiExploit
Config
--[[
AntiExploit Config
Tune these to match your game (WalkSpeed, JumpPower, map scale, etc).
This is a ModuleScript, child of AntiExploitModule.
]]
return {
CheckInterval = 0.3, -- seconds between checks per player (batched loop, not per-frame)
SpawnGracePeriod = 1.5, -- seconds after spawning before a player is checked (avoids load/respawn false flags)
Speed = {
ToleranceMultiplier = 1.5, -- allowed overshoot over Humanoid.WalkSpeed
Buffer = 6, -- flat extra studs/sec allowed on top (covers latency/animation drift)
FlagWeight = 1,
HardCapMultiplier = 3, -- instantaneous per-frame velocity cap (WalkSpeed * this); catches bursts
HardCapFlagWeight = 3, -- between periodic checks, since it's read every frame, not just every interval
},
Teleport = {
Threshold = 45, -- studs moved in one check window that speed alone can't explain
FlagWeight = 4,
},
Fly = {
GroundRayLength = 8, -- studs to raycast downward to decide "grounded"
MaxAirTimeMultiplier = 1.6, -- tolerance over the theoretical max rise-time of a legit jump
HoverVelocityThreshold = 1.5, -- studs/sec; |Y velocity| below this while airborne counts as "hovering"
HoverDuration = 0.75, -- seconds of hovering before flagging
FlagWeight = 3,
},
NoClip = {
FlagWeight = 3,
},
Punishment = {
FlagThreshold = 6, -- weighted score that triggers a kick
DecayPerSecond = 0.6, -- flags fade over time so a single lag spike can't add up into a false kick
},
}
FlagClass
--[[
FlagClass
Tracks a weighted "suspicion score" per player, per flag type, with decay over
time so a single lag spike or false positive fades out instead of stacking
toward a kick. This is a ModuleScript, child of AntiExploitModule.
]]
local FlagClass = {}
FlagClass.__index = FlagClass
function FlagClass.new()
return setmetatable({
Score = 0,
Counts = { Speed = 0, Teleport = 0, Fly = 0, NoClip = 0 },
_lastDecay = os.clock(),
}, FlagClass)
end
function FlagClass:Decay(decayPerSecond)
local now = os.clock()
local dt = now - self._lastDecay
self._lastDecay = now
if self.Score > 0 then
self.Score = math.max(0, self.Score - decayPerSecond * dt)
end
end
function FlagClass:Add(kind, weight)
self.Counts[kind] = (self.Counts[kind] or 0) + 1
self.Score += weight
end
return FlagClass
PlayerClass
--[[
PlayerClass
One instance per monitored player.
Movement is sampled continuously on Heartbeat (cheap: vector math and property
reads only, no raycasts) into running accumulators. Update() is then called on
a batched interval (see AntiExploitModule) and consumes those accumulators to
do the actual flagging plus the heavier raycast-based checks.
Why accumulate instead of just diffing start/end position every interval?
A cheater could burst-cheat for part of the interval and be legal for the rest,
so the two-point snapshot at the interval boundary can look clean even though
real cheating happened in between. Accumulating every frame closes that gap:
- Path-length accumulation catches "cheat then snap back" (both legs of the
round trip add distance; they don't cancel out like a net delta would).
- A per-frame instantaneous speed cap catches a naked burst even if it lasts
a fraction of a second.
- Airborne/rise/hover time for the fly check accumulates continuously too,
instead of only being sampled at the interval boundary.
Checks performed:
1) Teleport - accumulated path length too large for any legit speed to explain
2) Speed - accumulated horizontal path vs. what WalkSpeed allows for the window,
PLUS an instantaneous per-frame hard speed cap
3) Fly - accumulated sustained-rise time or hover time while airborne
4) NoClip - straight-line path between last/current checked position crosses a solid part
This is a ModuleScript, child of AntiExploitModule.
]]
local Workspace = game:GetService("Workspace")
local RunService = game:GetService("RunService")
local Utils = require(script.Parent.Utils)
local FlagClass = require(script.Parent.FlagClass)
local Config = require(script.Parent.Config)
local Maid = Utils.Maid
local groundRayParams = RaycastParams.new()
groundRayParams.FilterType = Enum.RaycastFilterType.Exclude
local PlayerClass = {}
PlayerClass.__index = PlayerClass
function PlayerClass.new(player)
local self = setmetatable({}, PlayerClass)
self.Player = player
self.Flags = FlagClass.new()
self._maid = Maid.new()
self.Character = nil
self.Humanoid = nil
self.RootPart = nil
self.LastCheckTime = os.clock()
self.SpawnTime = os.clock()
self._whitelistedUntil = 0
-- continuous accumulators, reset each time Update() consumes them
self._sampledPos = nil
self._accumPath = 0 -- total 3D path length traveled since last Update()
self._accumHorizPath = 0 -- total horizontal (XZ) path length traveled since last Update()
self._hardCapHits = 0 -- number of frames this window where instantaneous speed exceeded the hard cap
self._riseAccum = 0 -- seconds spent with sustained upward velocity while airborne, this window
self._hoverAccum = 0 -- seconds spent hovering (near-zero vertical velocity) while airborne, this window
self._airborneNow = false
self._maid:GiveTask(player.CharacterAdded:Connect(function(character)
self:_onCharacterAdded(character)
end), "CharacterAdded")
self._maid:GiveTask(RunService.Heartbeat:Connect(function(dt)
self:_onHeartbeat(dt)
end), "Heartbeat")
if player.Character then
self:_onCharacterAdded(player.Character)
end
return self
end
function PlayerClass:_onCharacterAdded(character)
local humanoid = character:WaitForChild("Humanoid", 5)
local root = character:WaitForChild("HumanoidRootPart", 5)
if not humanoid or not root then
return
end
self.Character = character
self.Humanoid = humanoid
self.RootPart = root
self.LastCheckTime = os.clock()
self.SpawnTime = os.clock()
self._sampledPos = root.Position
self._accumPath = 0
self._accumHorizPath = 0
self._hardCapHits = 0
self._riseAccum = 0
self._hoverAccum = 0
self._airborneNow = false
end
-- Call this from YOUR gameplay code right before you move a player yourself
-- (teleport pads, launch pads, elevators, cutscenes, dash abilities) so the
-- anti-exploit doesn't flag your own systems as cheating.
function PlayerClass:Whitelist(seconds)
self._whitelistedUntil = os.clock() + (seconds or 3)
end
function PlayerClass:IsWhitelisted()
return os.clock() < self._whitelistedUntil
end
local function getJumpVelocity(humanoid)
if humanoid.UseJumpPower then
return humanoid.JumpPower
end
local gravity = Workspace.Gravity
return math.sqrt(2 * math.max(gravity, 0.01) * math.max(humanoid.JumpHeight, 0))
end
-- Cheap per-frame sampler: vector math and property reads only. No raycasts, no
-- string building, no warnings here -- this runs for every monitored player every
-- frame, so it has to stay trivial.
function PlayerClass:_onHeartbeat(dt)
local humanoid, root = self.Humanoid, self.RootPart
if not humanoid or not root or not root.Parent then
return
end
if humanoid.Health <= 0 or self:IsWhitelisted() or (os.clock() - self.SpawnTime) < Config.SpawnGracePeriod then
self._sampledPos = root.Position
return
end
local pos = root.Position
local lastPos = self._sampledPos or pos
self._sampledPos = pos
local delta = pos - lastPos
self._accumPath += delta.Magnitude
self._accumHorizPath += Vector2.new(delta.X, delta.Z).Magnitude
-- Instantaneous hard speed cap: catches a burst even if it only lasts one frame,
-- regardless of when the periodic Update() happens to land.
local velocity = root.AssemblyLinearVelocity
local horizSpeed = Vector2.new(velocity.X, velocity.Z).Magnitude
local hardCap = (humanoid.WalkSpeed * Config.Speed.HardCapMultiplier) + Config.Speed.Buffer
if horizSpeed > hardCap then
self._hardCapHits += 1
end
-- Airborne state, tracked continuously so a brief fly-then-land can't hide between checks.
local airborne = humanoid.FloorMaterial == Enum.Material.Air
self._airborneNow = airborne
if airborne then
if velocity.Y > 1 then
self._riseAccum += dt
end
if math.abs(velocity.Y) < Config.Fly.HoverVelocityThreshold then
self._hoverAccum += dt
end
else
self._riseAccum = 0
self._hoverAccum = 0
end
end
function PlayerClass:Update()
local character, humanoid, root = self.Character, self.Humanoid, self.RootPart
if not character or not humanoid or not root or not root.Parent then
return
end
if humanoid.Health <= 0 then
return
end
local now = os.clock()
local dt = now - self.LastCheckTime
self.LastCheckTime = now
if dt <= 0 then
return
end
self.Flags:Decay(Config.Punishment.DecayPerSecond)
-- Snapshot and reset the accumulators for this window
local accumPath, accumHorizPath = self._accumPath, self._accumHorizPath
local hardCapHits = self._hardCapHits
local riseAccum, hoverAccum = self._riseAccum, self._hoverAccum
self._accumPath, self._accumHorizPath, self._hardCapHits = 0, 0, 0
local currentPos = root.Position
if (now - self.SpawnTime) < Config.SpawnGracePeriod or self:IsWhitelisted() then
return
end
-- 1) Teleport check: total path traveled this window too large for any legit speed to explain
-- (this also catches "cheat then snap back," since both legs of the round trip add distance)
if accumPath > Config.Teleport.Threshold then
self.Flags:Add("Teleport", Config.Teleport.FlagWeight)
warn(string.format("[AntiExploit] %s covered %.1f studs of path in %.2fs (teleport)", self.Player.Name, accumPath, dt))
else
-- 2) Speed check: accumulated horizontal path vs. what WalkSpeed allows for this window
local maxSpeed = (humanoid.WalkSpeed * Config.Speed.ToleranceMultiplier) + Config.Speed.Buffer
local maxDistance = maxSpeed * dt
if accumHorizPath > maxDistance then
self.Flags:Add("Speed", Config.Speed.FlagWeight)
warn(string.format("[AntiExploit] %s exceeded speed limit (%.1f/%.1f studs)", self.Player.Name, accumHorizPath, maxDistance))
end
end
-- 2b) Instantaneous hard speed cap breaches, caught between checks
if hardCapHits > 0 then
self.Flags:Add("Speed", Config.Speed.HardCapFlagWeight)
warn(string.format("[AntiExploit] %s hit the instantaneous speed cap %d time(s) (burst speed)", self.Player.Name, hardCapHits))
end
-- 3) Fly check, from continuously-accumulated airborne time (not just a snapshot at this tick)
local jumpVelocity = getJumpVelocity(humanoid)
local maxRiseDuration = (jumpVelocity / math.max(Workspace.Gravity, 0.01)) * Config.Fly.MaxAirTimeMultiplier
if riseAccum > maxRiseDuration then
self.Flags:Add("Fly", Config.Fly.FlagWeight)
warn(string.format("[AntiExploit] %s accumulated %.2fs of sustained ascent (possible fly)", self.Player.Name, riseAccum))
end
if hoverAccum > Config.Fly.HoverDuration then
self.Flags:Add("Fly", Config.Fly.FlagWeight)
warn(string.format("[AntiExploit] %s accumulated %.2fs of hovering (possible fly)", self.Player.Name, hoverAccum))
end
-- 4) Basic noclip check: did the straight-line path from last checked position to now cross something solid?
local lastCheckedPos = self._lastCheckedPos or currentPos
self._lastCheckedPos = currentPos
local straightDelta = currentPos - lastCheckedPos
if straightDelta.Magnitude > 2 then
groundRayParams.FilterDescendantsInstances = { character }
local hit = Workspace:Raycast(lastCheckedPos, straightDelta, groundRayParams)
if hit and hit.Instance and hit.Instance.CanCollide then
self.Flags:Add("NoClip", Config.NoClip.FlagWeight)
warn(string.format("[AntiExploit] %s passed through %s (possible noclip)", self.Player.Name, hit.Instance:GetFullName()))
end
end
end
function PlayerClass:Destroy()
self._maid:DoCleaning()
end
return PlayerClass
Utils
--[[
Utils
A small Maid (cleanup) helper. Kept from the old module because it's a solid,
well-known pattern for managing connections/instances without leaking them.
This is a ModuleScript, child of AntiExploitModule.
]]
local Maid = {}
Maid.__index = Maid
function Maid.new()
return setmetatable({ _tasks = {}, _counter = 0 }, Maid)
end
local function cleanupTask(t)
if typeof(t) == "function" then
t()
elseif typeof(t) == "RBXScriptConnection" then
t:Disconnect()
elseif typeof(t) == "Instance" then
t:Destroy()
elseif type(t) == "table" and typeof(t.Destroy) == "function" then
t:Destroy()
end
end
-- Give the maid a task to clean up later. Pass a key to replace/clean up a previous
-- task under that same key (e.g. "CharacterAdded"); omit it for an auto-generated key.
function Maid:GiveTask(taskItem, key)
if key == nil then
self._counter += 1
key = self._counter
elseif self._tasks[key] ~= nil then
cleanupTask(self._tasks[key])
end
self._tasks[key] = taskItem
return key
end
function Maid:Remove(key)
local t = self._tasks[key]
if t then
cleanupTask(t)
self._tasks[key] = nil
end
end
function Maid:DoCleaning()
for key, t in pairs(self._tasks) do
cleanupTask(t)
self._tasks[key] = nil
end
end
Maid.Destroy = Maid.DoCleaning
return {
Maid = Maid,
}
