Four independent bugs kept the car immobile at the world origin: 1. Wheels collided with their own chassis. Wheel inner edge sits at 2.80 vs body edge 3.00, and wheel top -0.20 vs body bottom -0.70. The overlap is intentional styling, but in one collision group the wheels ground against the body: motors spun ~1 rad/s against 20,000 torque. Wheels now use a CarWheel group that ignores CarChassis. 2. Suspension exploded on spawn. PrismaticConstraint+SpringConstraint between a 0.6-stud hub and the ballasted body was unstable enough to fling knuckles ~10,000,000 studs out, past FallenPartsDestroyHeight, deleting the wheels. Replaced with a rigid welded hub mount. 3. Car spawned at world origin, not the pad. MapBuilder made a SpawnPad part but no SpawnLocation, so characters spawned at (0,0,0) and Sit() dragged the car to them. Added a real SpawnLocation and the model is now pivoted after parenting, with the character teleported first. 4. AntiCheat snapped that legitimate spawn teleport back. An ~8,500-stud server move reads as a teleport hack. Added AntiCheat.grant() so server-initiated moves are exempt for a short grace window. Verified in Studio: 881 studs in 6s, 496 studs/s top speed, 152 deg steering, braking 33 -> 0 studs/s, chassis stays upright.
96 lines
3.0 KiB
Luau
96 lines
3.0 KiB
Luau
--!strict
|
|
--[[
|
|
AntiCheat.luau (SERVER)
|
|
MVP-level movement sanity check. Not a fortress — a speed bump that catches
|
|
obvious speed/teleport hacks. Because money & car ownership are ALREADY
|
|
server-authoritative (Economy validates everything), the main client attack
|
|
surface left is movement, so we watch that.
|
|
|
|
Approach: sample each player's HRP position periodically; if they cover
|
|
ground implying a sustained speed above MaxPlausibleSpeed, snap them back
|
|
to their last valid position. Generous threshold to avoid false positives
|
|
from legit fast cars.
|
|
]]
|
|
|
|
local Players = game:GetService("Players")
|
|
local RunService = game:GetService("RunService")
|
|
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
|
|
|
local GameConfig = require(ReplicatedStorage.Shared.GameConfig)
|
|
|
|
local AntiCheat = {}
|
|
|
|
type Track = { lastPos: Vector3, lastValid: CFrame }
|
|
|
|
local tracked: { [Player]: Track } = {}
|
|
local SAMPLE_INTERVAL = 0.5
|
|
|
|
-- Server-initiated teleports (car spawn onto the pad, respawn, mode resets)
|
|
-- legitimately move a player thousands of studs in one frame, which trips the
|
|
-- speed check and snaps them straight back -- dragging their car with them.
|
|
-- Callers announce those moves via AntiCheat.grant() so the next sample is
|
|
-- accepted as the new baseline instead of treated as a hack.
|
|
local graceUntil: { [Player]: number } = {}
|
|
local GRACE_SECONDS = 2
|
|
|
|
--- Exempt a player from the speed check briefly after a legitimate teleport.
|
|
function AntiCheat.grant(player: Player, seconds: number?)
|
|
graceUntil[player] = os.clock() + (seconds or GRACE_SECONDS)
|
|
end
|
|
|
|
local function hrpOf(player: Player): BasePart?
|
|
local char = player.Character
|
|
return char and char:FindFirstChild("HumanoidRootPart") :: BasePart?
|
|
end
|
|
|
|
function AntiCheat.init()
|
|
local function watch(player: Player)
|
|
player.CharacterAdded:Connect(function(char)
|
|
local hrp = char:WaitForChild("HumanoidRootPart", 10) :: BasePart?
|
|
if hrp then
|
|
tracked[player] = { lastPos = hrp.Position, lastValid = hrp.CFrame }
|
|
end
|
|
end)
|
|
end
|
|
for _, p in ipairs(Players:GetPlayers()) do
|
|
watch(p)
|
|
end
|
|
Players.PlayerAdded:Connect(watch)
|
|
Players.PlayerRemoving:Connect(function(p)
|
|
tracked[p] = nil
|
|
end)
|
|
|
|
local accum = 0
|
|
RunService.Heartbeat:Connect(function(dt)
|
|
accum += dt
|
|
if accum < SAMPLE_INTERVAL then
|
|
return
|
|
end
|
|
local interval = accum
|
|
accum = 0
|
|
|
|
for player, track in pairs(tracked) do
|
|
local hrp = hrpOf(player)
|
|
if not hrp then
|
|
continue
|
|
end
|
|
local pos = hrp.Position
|
|
-- Falling doesn't count (avoid flagging big drops). Only guard XZ plane.
|
|
local horizontal = Vector3.new(pos.X - track.lastPos.X, 0, pos.Z - track.lastPos.Z).Magnitude
|
|
local hSpeed = horizontal / interval
|
|
|
|
local inGrace = (graceUntil[player] or 0) > os.clock()
|
|
if hSpeed > GameConfig.MaxPlausibleSpeed and not inGrace then
|
|
-- Snap back: likely a speed/teleport hack.
|
|
hrp.CFrame = track.lastValid
|
|
hrp.AssemblyLinearVelocity = Vector3.zero
|
|
else
|
|
track.lastValid = hrp.CFrame
|
|
end
|
|
track.lastPos = hrp.Position
|
|
end
|
|
end)
|
|
end
|
|
|
|
return AntiCheat
|