From 45069188be1add8d05d397587183c59b83c25b17 Mon Sep 17 00:00:00 2001 From: "R3AL.CH1CK3N" Date: Fri, 31 Jul 2026 13:00:33 -0400 Subject: [PATCH] fix(CarManager): repair car torn apart on spawn by client-owned character Sit() dragged the seat's body assembly ~15,700 studs to the client-owned character while the knuckle/wheel assemblies stayed at the spawn pad, tearing the car apart; wheels then fell out of the world. Server-side character:PivotTo() does not stick (client has authority) -- it holds one frame and snaps back, so the pre-seat teleport could not fix this. Instead: record each part's offset from the body while the model is intact, take server network ownership, let Sit() and the snap-back settle, then reassemble every assembly around the body (forced level) and hand ownership back to the driver. Verified in Studio: max part separation 6.42 studs (exhaust, normal), up-vector 1.00, driver seated, drove 98.6 studs at 41 studs/s intact. --- src/server/CarManager.luau | 74 +++++++++++++++++++++++++++++++++++--- 1 file changed, 70 insertions(+), 4 deletions(-) diff --git a/src/server/CarManager.luau b/src/server/CarManager.luau index 3255cc0..8d55b10 100644 --- a/src/server/CarManager.luau +++ b/src/server/CarManager.luau @@ -350,14 +350,80 @@ function CarManager.spawn(player: Player, carId: string?) if character then local seat = model:FindFirstChild(Chassis.SEAT_NAME) :: VehicleSeat if seat then - -- Teleport the character to the car before seating. Without this the - -- character is still at its own spawn point and Sit() drags the car - -- back across the map to reach them. Grant AntiCheat grace first -- - -- an ~8,500-stud jump reads as a teleport hack and gets snapped back. + -- The character is CLIENT-owned, so a server-side PivotTo of it does + -- NOT stick: it holds for one frame and the client snaps it back to + -- where it was. Sit() then drags the seat's assembly across the map + -- to reach the character -- and because a constraint chassis keeps + -- the body, knuckles and wheels as SEPARATE assemblies, only the + -- body follows. The knuckles/wheels stay behind at the pad and the + -- car is torn apart (measured ~15,700 studs), later falling out of + -- the world. + -- + -- Fix: don't fight the client. Record every part's offset from the + -- body while the model is still intact, let Sit() and the character + -- snap-back fully settle, then REASSEMBLE the car around wherever + -- the body actually ended up. + local primary = model.PrimaryPart + local offsets: { [BasePart]: CFrame } = {} + local movable: { BasePart } = {} + if primary then + local inverse = primary.CFrame:Inverse() + for _, desc in ipairs(model:GetDescendants()) do + if desc:IsA("BasePart") and not desc.Anchored then + offsets[desc] = inverse * desc.CFrame + table.insert(movable, desc) + end + end + end + AntiCheat.grant(player) character:PivotTo(target * CFrame.new(0, 3, 0)) task.defer(function() seat:Sit(character:FindFirstChildOfClass("Humanoid")) + + if not primary then + return + end + -- Hold server authority while the rig is in pieces, otherwise + -- the client re-asserts the torn layout over our repair. + for _, part in ipairs(movable) do + pcall(function() + part:SetNetworkOwner(nil) + end) + end + + task.wait(1.2) -- Sit() + client snap-back settle here + + if not primary:IsDescendantOf(Workspace) then + return + end + -- Rebuild every assembly around the body, forced level so the + -- car never lands on its roof. + local position = primary.Position + local look = primary.CFrame.LookVector + local flat = Vector3.new(look.X, 0, look.Z) + if flat.Magnitude < 0.1 then + flat = Vector3.new(0, 0, -1) + end + local base = CFrame.lookAt(position, position + flat.Unit) + for _, part in ipairs(movable) do + if part:IsDescendantOf(Workspace) then + part.CFrame = base * offsets[part] + part.AssemblyLinearVelocity = Vector3.zero + part.AssemblyAngularVelocity = Vector3.zero + end + end + + task.wait(0.3) -- let the solver accept the repaired rig + + AntiCheat.grant(player) + for _, part in ipairs(movable) do + if part:IsDescendantOf(Workspace) then + pcall(function() + part:SetNetworkOwner(player) + end) + end + end end) end end