How to add server-side distance checks in Roblox
Chests, shops and sell pads that work from anywhere on the map. How to check on the server that the player's character is close enough to use them.
What it looks like
The client asks to use something that sits in the world: open a chest, sell at a sell pad, buy from a stall, pick up an item, talk to an NPC. The server does it without checking that the player's character is anywhere near.
Luau
remote.OnServerEvent:Connect(function(player, chest) openChest(player, chest) end)
How to detect it
- Find handlers tied to a place in the world: they take a world Instance or the id of one, or they're only meant to work while the player stands somewhere, such as a button that only shows on a sell pad.
- Look for a server-side distance check between the character's
HumanoidRootPartand the object or area. - Checks done on the client, such as only firing the remote from a
Touchedevent or only showing the button nearby, don't count. The client can fire the remote from anywhere.
What an exploiter could do
They use things from anywhere on the map: collect every chest without travelling, sell from spawn, or buy from a shop that's meant to be at the end of a level. Usually that's a time saving, but when reaching the place is the challenge it lets them skip progress.
How to fix it
- On the server, check the distance from the character's
HumanoidRootPartto the object, or to a part that marks the area, and allow a small margin for latency. - The character's position comes from the player's own client, so this stops use from a distance but not a client that also moves its character. Movement checks belong to anti-exploit.
Vulnerable example
Vulnerable
local Players = game:GetService("Players") local ReplicatedStorage = game:GetService("ReplicatedStorage") local Workspace = game:GetService("Workspace") local PlayerData = require(script.Parent.PlayerData) local chests = Workspace:WaitForChild("Chests") local reward = 50 local respawn = 600 local opened: { [Player]: { [Instance]: number } } = {} ReplicatedStorage.Remotes.OpenChest.OnServerEvent:Connect(function(player: Player, chest: unknown) if typeof(chest) ~= "Instance" or not chest:IsA("BasePart") or chest.Parent ~= chests then return end local profile = PlayerData.get(player) if not profile then return end local times = opened[player] or {} opened[player] = times local now = os.clock() if times[chest] and now - times[chest] < respawn then return end times[chest] = now profile.Coins += reward end) Players.PlayerRemoving:Connect(function(player) opened[player] = nil end)
Fixed example
Fixed
local Players = game:GetService("Players") local ReplicatedStorage = game:GetService("ReplicatedStorage") local Workspace = game:GetService("Workspace") local PlayerData = require(script.Parent.PlayerData) local chests = Workspace:WaitForChild("Chests") local reward = 50 local respawn = 600 local reach = 12 local opened: { [Player]: { [Instance]: number } } = {} ReplicatedStorage.Remotes.OpenChest.OnServerEvent:Connect(function(player: Player, chest: unknown) if typeof(chest) ~= "Instance" or not chest:IsA("BasePart") or chest.Parent ~= chests then return end local character = player.Character local root = character and character:FindFirstChild("HumanoidRootPart") if not root or not root:IsA("BasePart") or (root.Position - chest.Position).Magnitude > reach then return end local profile = PlayerData.get(player) if not profile then return end local times = opened[player] or {} opened[player] = times local now = os.clock() if times[chest] and now - times[chest] < respawn then return end times[chest] = now profile.Coins += reward end) Players.PlayerRemoving:Connect(function(player) opened[player] = nil end)
Common false positives
- Menus meant to work anywhere, such as a shop button on the HUD, don't need a distance check.
- Actions on the player's own things that give them nothing new, like rearranging furniture on their own plot, don't need one either.
- A distance check inside a helper module counts. Read it before reporting.