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.
This commit is contained in:
@@ -350,14 +350,80 @@ function CarManager.spawn(player: Player, carId: string?)
|
|||||||
if character then
|
if character then
|
||||||
local seat = model:FindFirstChild(Chassis.SEAT_NAME) :: VehicleSeat
|
local seat = model:FindFirstChild(Chassis.SEAT_NAME) :: VehicleSeat
|
||||||
if seat then
|
if seat then
|
||||||
-- Teleport the character to the car before seating. Without this the
|
-- The character is CLIENT-owned, so a server-side PivotTo of it does
|
||||||
-- character is still at its own spawn point and Sit() drags the car
|
-- NOT stick: it holds for one frame and the client snaps it back to
|
||||||
-- back across the map to reach them. Grant AntiCheat grace first --
|
-- where it was. Sit() then drags the seat's assembly across the map
|
||||||
-- an ~8,500-stud jump reads as a teleport hack and gets snapped back.
|
-- 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)
|
AntiCheat.grant(player)
|
||||||
character:PivotTo(target * CFrame.new(0, 3, 0))
|
character:PivotTo(target * CFrame.new(0, 3, 0))
|
||||||
task.defer(function()
|
task.defer(function()
|
||||||
seat:Sit(character:FindFirstChildOfClass("Humanoid"))
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user