Is this anticheat good

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,
}

Why does it use so much bloat and oop slop?
It should use none of it also if you are going to do that then just use server authority at this point.

2 Likes

Don’t use claude for Anticheat stuff it could flag normal players instead of hackers like it can make many mistakes and ban normal people who did nothing