Is this is a okay anti cheat / anti exploit?

It’s real basic I know and it doesn’t consider / cover much. Plus I haven’t tested it out. What do you guys think? Hm well I think it looks ok. Any suggestions or mistakes I made?

Here’s the uh code:

local DataStoreService = game:GetService("DataStoreService")
-- You could integrate this with your existing data store if you have one
local DataStore = DataStoreService:GetDataStore("DataStore")

local Tracker = {}
Tracker.__index = Tracker
Tracker.Index = {}

-- Globals

-- Just in case they leave / disconnect, we'll log their UserIds
local ListOfPlayers = {}
local MAX_STRIKES = 5

-- Utility Functions

local function yieldCallback(waitTime: number, callback: any)
    if type(callback) ~= "function" then return end
    task.wait(waitTime)
    callback()
    waitTime = nil
end

-- Class Functions

function Tracker.new(Player: Player)
    DataStore:UpdateAsync(Player.UserId, function(oldValue)
        -- Check to see if the player is banned
        if not oldValue then
            return {["IsBanned"] = false}
        end
        if oldValue.IsBanned == true then Player:Kick("You're banned. That's it.") end
    end)
    local self = setmetatable({}, Tracker)
    self.__index = Tracker
    self.Player = Player
    self.LoggedName = Player.Name
    self.Strikes = 0
    self.PositionHistory = {}
    self.States = {}
    Tracker.Index[Player.Name] = self
    -- If a change occurs, we'll always kinda will have the updated changes
    Tracker.Index[Player.Name].__index = self
    ListOfPlayers[Player.Name] = Player.UserId
    return self
end

function Tracker:GetPlayerTracker(playerName: string)
    return Tracker.Index[playerName]
end

function Tracker:RemoveTracker(playerName: string)
    if Tracker.Index[playerName] then
        Tracker.Index[playerName].__index, Tracker.Index[playerName], playerName = nil, nil, nil
    end
end

-- Object Instance Functions

function Tracker:BanPlayer()
    if not self.Player and self.LoggedName and ListOfPlayers[self.LoggedName] then
        local Player = ListOfPlayers[self.LoggedName]
        DataStore:UpdateAsync(Player.UserId, function(oldValue)
            return {["IsBanned"] = true}
        end)
    end
end

function Tracker:AddStrike()
    if not self.Strikes then return end
    self.Strikes += 1
    warn(`Strike given to {self.Player.Name}`)
    warn(`Strike total is at {self.Strikes}`)
    if self.Strikes == MAX_STRIKES then self.Player:Kick("You've been flagged by our system too many times. This is not a ban message.") end
end

function Tracker:LogPlayerState()
    if not self.Player or not self.PositionHistory or not self.States then return end
    if self.Player.Character.Humanoid:GetState() == nil then
        warn(`Mismatched player states for {self.Player.Name}`)
        return self:AddStrike()
    end
    local State = self.Player.Character.Humanoid:GetState()
    table.insert(self.PositionHistory, self.Player.Character.HumanoidRootPart.CFrame)
    self.States[State] = State
    State = nil
end

function Tracker:RollbackPosition()
    if not self.Player or not self.PositionHistory or not self.PositionHistory[#self.PositionHistory] then return end
    self.Player.Character.HumanoidRootPart.CFrame = self.PositionHistory[#self.PositionHistory]
end

function Tracker:PositionSanityCheck()
    if not self.PositionHistory or not self.PositionHistory[#self.PositionHistory] or not self.PositionHistory[#self.PositionHistory - 1] then return end
    local CurrentPosition = self.PositionHistory[#self.PositionHistory]
    local PreviousPosition = self.PositionHistory[#self.PositionHistory - 1]
    local Distance = (CurrentPosition.Position - PreviousPosition.Position).Magnitude
    if (self.Player.Character.Humanoid.WalkSpeed - (Distance / 5)) > 2 then return self:AddStrike() end
    CurrentPosition, PreviousPosition, Distance = nil, nil, nil
end

function Tracker:IsFlying()
    if not self.Player then return end
    if self.Player.Character.Humanoid.FloorMaterial == Enum.Material.Air then
        return true
    else return false
    end
end

function Tracker:IsNoClipping()
    if not self.Player then return end
    local Params = RaycastParams.new()
    Params.FilterType = Enum.RaycastFilterType.Include
    Params.FilterDescendantsInstances = self.Player.Character:GetChildren()
    -- Cast a ray in the look direction of the player multiplied by 2 (so 2 studs forward)
    local Cast1 = workspace:Raycast(self.Player.Character.HumanoidRootPart.Position, self.Player.Character.HumanoidRootPart.CFrame.LookVector * 2, Params)
    -- Cast a ray in the opposite of the look direction of the player multiplied by 2 (so 2 studs backwards)
    local Cast2 = workspace:Raycast(self.Player.Character.HumanoidRootPart.Position, -self.Player.Character.HumanoidRootPart.CFrame.LookVector * 2, Params)
    if Cast1 and Cast2 then
        warn(`{self.Player.Name} is possibly no-clipping (could be innocent no-clipping)`)
        Cast1, Cast2, Params = nil, nil, nil
        return self:AddStrike()
    end
end

return Tracker
5 Likes

Is there a reason you switch back and forth between Player.Name and Player.UserId?

1 Like

Yeah like for the DataStore I wouldn’t want someone just changing their username (unlikely tho)

That’s why I switched to Player.UserId for some lines

I have an idea on how I’d improve this. I would have a for loop that runs five times (waits 1 second each time it loops) and it would get the player’s distance from the floor.

Then it would get the fall speed

HumanoidRootPart.AssemblyLinearVelocity.Y

And then in the next loop, it would do the same but it would check if the player has fallen a reasonable amount in relation to the fall speed (and the previous distance from the floor).

If it’s not, I’ll flag them a bit and send them back to when they were on the floor

1 Like

it would look something like this (this is pseudo code btw)


(yes this was all typed on mobile)
local function getDictionaryLen(t)
   local Length = 0
   for i, v in pairs(t) do
      Length += 1
   end
   return Length
end

function Tracker:FlyCheck()
    if not self.Player then return end
    if self.Player.Character.Humanoid.FloorMaterial == Enum.Material.Air then
       local PositionHistory = {}
       local FlyStrikes = 0
       local MAX_FLYSTRIKES = 3
       local FloorDistance, FallSpeed
       for i = 1, 5 do
          task.wait(1)
             local Params = RaycastParams.new()
             Params.FilterType = Enum.RaycastFilterType.Include
             Params.FilterDescendantsInstances = self.Player.Character:GetChildren()
--[[ gonna assume that they can’t go
1000 studs in the air
]]
             local Cast = workspace:Raycast(self.Player.Character.HumanoidRootPart.Position, -self.Player.Character.HumanoidRootPart.CFrame.UpVector * 1000, Params)
-- -UpVector = DownVector
             if not Cast then return end
             FloorDistance = (self.Player.Character.HumanoidRootPart.Position - Cast.Position)
             FallSpeed = self.Character.HumanoidRootPart.AssemblyLinearVelocity.Y
         if getDictionaryLength(PositionHistory) == 0 then
            PositionHistory[tostring(i)] = FloorDistance
            elseif getDictionaryLength(PositionHistory) > 0 then
               local Previous = PositionHistory[getDictionaryLength(PositionHistory)]
              if (FloorDistance - Previous) < (FallSpeed - 1) then
              -- less than the fall speed - 1 (for laggy people)
              FlyStrikes += 1
             end
             if FlyStrikes == MAX_FLYSTRIKES then
                -- 3 strikes just cause
                self:AddStrike()
                self:AddStrike()
                self:AddStrike()
                return
             end
         end
       end
    else return
    end
end