--!strict --[[ CarManager.luau (SERVER) Spawns the player's equipped car as a CONSTRAINT CHASSIS vehicle. M1 upgrade: the old single-Part + VehicleSeat arcade car is replaced with a proper physical chassis (A-Chassis style, built from Roblox constraints): Body ──Prismatic(vertical)+Spring──> Hub ──[front only: Servo hinge]──> Knuckle ──Motor hinge──> Wheel - Springs give real suspension; prismatics constrain travel to vertical. - Front knuckles yaw on Servo hinges (steering). - Every wheel spins on a Motor hinge (drive/brake). - The VehicleSeat is kept ONLY as the input device: its Throttle/Steer are read by the client ChassisController, which sets motor/servo targets locally (the driver owns the physics, so it replicates). The real RX-7 mesh swap (docs/MESH_SWAP.md) welds a MeshPart onto the Body and hides the placeholder blocks — the chassis itself doesn't change. ]] local Players = game:GetService("Players") local Workspace = game:GetService("Workspace") local ReplicatedStorage = game:GetService("ReplicatedStorage") local Shared = ReplicatedStorage.Shared local Cars = require(Shared.Cars) local Chassis = require(Shared.Chassis) local GameConfig = require(Shared.GameConfig) local DataManager = require(script.Parent.DataManager) local CarBody = require(script.Parent.CarBody) local AntiCheat = require(script.Parent.AntiCheat) local PhysicsService = game:GetService("PhysicsService") type Car = Cars.Car type Tuning = Chassis.Tuning local CarManager = {} local activeCars: { [Player]: Model } = {} local carsFolder: Folder -- Spawn seating -------------------------------------------------------------- -- How far above the character's root the car body is placed before seating, so -- the freshly teleported chassis doesn't start interpenetrating the ground. local SEAT_LIFT = 3 -- Grace period after Sit() before network ownership is handed to the client. -- The rig must be fully settled first or the client re-asserts a stale layout. local SETTLE_TIME = 1.0 -- How far to sink an imported mesh shell into the chassis so it doesn't look -- like it's hovering. Purely cosmetic (shell parts are Massless/CanCollide=false). -- Sink the shell slightly so the tyres aren't fully swallowed by the bodywork. -- Every stud of drop is a stud of cabin height lost, and hyper_x (lowest roof -- in the library at 4.64) has none to spare, so keep this small. local SHELL_DROP = 0.25 -- Collision groups ------------------------------------------------------------ -- The wheels deliberately overlap the chassis: with BODY_SIZE.X = 6 the body -- edge sits at 3.0, while a wheel at TRACK_HALF 3.35 with WHEEL_WIDTH 1.1 has -- its inner edge at 2.80 -- a 0.2 stud lateral interpenetration. Vertically the -- wheel top reaches -0.2 against a body bottom of -0.7. Left in the same -- collision group the wheels grind against the body and the car will not roll -- (motors spin at ~1 rad/s against 20,000 torque). Putting wheels in their own -- group that ignores the chassis is the fix; the geometry is intentional. local WHEEL_GROUP = "CarWheel" local CHASSIS_GROUP = "CarChassis" -- Seating the driver on the cabin FLOOR (see Chassis.SEAT_ABOVE_GROUND) tucks -- the avatar down inside the body slab, so its legs/torso now genuinely -- interpenetrate the chassis. Overlapping colliders is exactly what launches a -- character across the map, so the driver gets its own group that ignores the -- car. The driver still collides with the world, so it can't clip through -- terrain when ejected. local DRIVER_GROUP = "CarDriver" local groupsReady = false local function ensureCollisionGroups() if groupsReady then return end groupsReady = true local ok = pcall(function() PhysicsService:RegisterCollisionGroup(WHEEL_GROUP) PhysicsService:RegisterCollisionGroup(CHASSIS_GROUP) PhysicsService:RegisterCollisionGroup(DRIVER_GROUP) end) if not ok then -- Already registered (Play-solo re-entry) -- harmless. end PhysicsService:CollisionGroupSetCollidable(WHEEL_GROUP, CHASSIS_GROUP, false) PhysicsService:CollisionGroupSetCollidable(CHASSIS_GROUP, CHASSIS_GROUP, true) PhysicsService:CollisionGroupSetCollidable(WHEEL_GROUP, WHEEL_GROUP, false) PhysicsService:CollisionGroupSetCollidable(DRIVER_GROUP, CHASSIS_GROUP, false) PhysicsService:CollisionGroupSetCollidable(DRIVER_GROUP, WHEEL_GROUP, false) end --- Put every part of a character into the driver collision group (or back into --- Default when they get out), so a low-seated avatar can't fight the chassis. local function setCharacterGroup(character: Model?, group: string) if not character then return end for _, d in ipairs(character:GetDescendants()) do if d:IsA("BasePart") then pcall(function() d.CollisionGroup = group end) end end end -- Helpers --------------------------------------------------------------------- local function makeAttachment(parent: BasePart, name: string, cf: CFrame): Attachment local att = Instance.new("Attachment") att.Name = name att.CFrame = cf att.Parent = parent return att end -- Attachment whose primary (X) axis points straight up — used for vertical -- prismatic travel and for the steering servo's yaw axis. local VERTICAL_AXIS = CFrame.Angles(0, 0, math.rad(90)) local function makePart(props: { Name: string, Size: Vector3, CFrame: CFrame, Color: Color3?, Material: Enum.Material?, CanCollide: boolean, Parent: Instance, Shape: Enum.PartType?, Friction: number?, }): Part local part = Instance.new("Part") part.Name = props.Name part.Size = props.Size part.CFrame = props.CFrame if props.Shape then part.Shape = props.Shape end part.Color = props.Color or Color3.new(0.2, 0.2, 0.2) part.Material = props.Material or Enum.Material.SmoothPlastic part.CanCollide = props.CanCollide if props.Friction then part.CustomPhysicalProperties = PhysicalProperties.new(0.9, props.Friction, 0.2, 1, 1) end part.Parent = props.Parent return part end -- Build one corner of the suspension. `isFront` corners get a steering servo. -- All positions are in BODY-LOCAL space, converted via body.CFrame. local function buildCorner(model: Model, body: Part, tuning: Tuning, xSign: number, zSign: number) -- FORWARD CONVENTION: car forward is body-local -Z. This is the Roblox -- standard -- CFrame.LookVector IS the -Z basis vector. -- -- DO NOT "fix" this to `zSign > 0`. That was tried on 2026-08-02 and made -- things strictly worse: 2 of 5 cars were backwards before, all 5 after. -- It was justified by tools/diag_seatdir.luau reporting "throttle drives -- toward local +Z", which was a SIGN ERROR in the diagnostic, not a fact -- about the game: expressing a part's own LookVector in its own local -- frame is (0,0,-1) BY DEFINITION, so the script mixed world and local -- space and then "confirmed" the inversion it was looking for. -- -- Lesson: a measurement that merely restates a definition cannot be -- evidence. Before trusting a diagnostic, ask what result would DISPROVE -- the hypothesis -- if no possible output would, the check is worthless. local isFront = zSign < 0 local cornerName = (isFront and "F" or "R") .. (xSign < 0 and "L" or "R") local wheelLocal = Vector3.new(xSign * Chassis.TRACK_HALF, -Chassis.SUSPENSION_DROP, zSign * Chassis.WHEELBASE_HALF) local wheelWorld = body.CFrame * CFrame.new(wheelLocal) -- Hub: rides the prismatic+spring below the body corner --------------------- local hub = makePart({ Name = "Hub_" .. cornerName, Size = Vector3.new(0.6, 0.6, 0.6), CFrame = wheelWorld, CanCollide = false, Parent = model, }) local bodyAtt = makeAttachment(body, "SuspAtt_" .. cornerName, CFrame.new(wheelLocal) * VERTICAL_AXIS) local hubAtt = makeAttachment(hub, "HubAtt", VERTICAL_AXIS) -- Rigid hub mount. A PrismaticConstraint+SpringConstraint suspension was used -- here, but the hub (0.6 cube) is ~3 orders of magnitude lighter than the -- ballasted body, and that mass ratio across a stiff spring makes the solver -- diverge: the hub/knuckle/wheel assemblies get flung to ~1e7 studs, fall -- past FallenPartsDestroyHeight and are deleted -- leaving a car with a body -- and no wheels that cannot move. Welding the hub makes it part of the body -- assembly, which is rock stable. Ride is rigid; the car actually drives. local hubWeld = Instance.new("WeldConstraint") hubWeld.Name = "SuspWeld_" .. cornerName hubWeld.Part0 = body hubWeld.Part1 = hub hubWeld.Parent = body -- Front corners: a knuckle that yaws on a servo hinge (steering) ------------ local wheelParent: Part = hub if isFront then -- Knuckle size is load-bearing, not cosmetic: it sets the mass the -- steering servo actually has to work with. See Chassis.KNUCKLE_SIZE -- for the measured sweep -- at the old 0.5 the servo overshot its own -- limits by 70+ degrees and the front wheels were uncontrollable. -- It is CanCollide=false and fully transparent, so growing it changes -- physics authority only, never the look of the car. local knuckle = makePart({ Name = "Knuckle_" .. cornerName, Size = Vector3.new(Chassis.KNUCKLE_SIZE, Chassis.KNUCKLE_SIZE, Chassis.KNUCKLE_SIZE), CFrame = wheelWorld, CanCollide = false, Parent = model, }) knuckle.Transparency = 1 local hubSteerAtt = makeAttachment(hub, "SteerAtt", VERTICAL_AXIS) local knuckleAtt = makeAttachment(knuckle, "KnuckleAtt", VERTICAL_AXIS) local servo = Instance.new("HingeConstraint") servo.Name = Chassis.STEER_SERVO_NAME servo.Attachment0 = hubSteerAtt servo.Attachment1 = knuckleAtt servo.ActuatorType = Enum.ActuatorType.Servo servo.ServoMaxTorque = 1e6 servo.AngularSpeed = tuning.SteerSpeed servo.TargetAngle = 0 servo.LimitsEnabled = true servo.LowerAngle = -tuning.MaxSteerAngle servo.UpperAngle = tuning.MaxSteerAngle servo.Parent = hub wheelParent = knuckle end -- Wheel: cylinder spinning on a motor hinge about the lateral (X) axis ------ -- VISIBILITY: this cylinder is the PHYSICS wheel and must be invisible when -- a real imported shell is used, because that shell brings its own modelled -- wheels. Leaving it opaque put TWO sets of wheels on every car -- the mesh's -- tyre plus a grey rubber cylinder on top of it -- which is what read as -- "way too wide". They don't even line up: the cylinder's outer edge sits at -- X=4.10 while the gtr_r34 mesh tyre sits at 4.52, so the duplicate poked out -- past the real tyre at a different track width. The shell is applied later -- in buildCarModel and hides these; if NO shell exists the procedural CarBody -- kit supplies rims, so the cylinder is never the intended visual either way. local wheel = makePart({ Name = "Wheel_" .. cornerName, Size = Vector3.new(Chassis.WHEEL_WIDTH, Chassis.WHEEL_RADIUS * 2, Chassis.WHEEL_RADIUS * 2), CFrame = wheelWorld, Shape = Enum.PartType.Cylinder, -- cylinder axis = X = lateral. correct spin axis Color = Color3.new(0.08, 0.08, 0.09), Material = Enum.Material.Rubber, CanCollide = true, Friction = tuning.WheelFriction, Parent = model, }) local parentAtt = makeAttachment(wheelParent, "WheelAxleAtt", CFrame.new()) local wheelAtt = makeAttachment(wheel, "WheelAtt", CFrame.new()) -- STEERING/MOTOR CONFLICT -- UNRESOLVED, see docs note below. -- -- A steered front corner is two hinges in series: Hub --SteerServo(Y)--> -- Knuckle --WheelMotor(X)--> Wheel. Measured (tools/diag_steer4.luau, -- gtr_r34, target -20 deg): the servo tracks to within 0.01 deg ONLY when -- the front wheel is detached or its hinge disabled; with the wheel -- attached it stays pinned near 0 deg even hoisted in free air with -- ServoMaxTorque up to 1e9. So it is NOT grip and NOT a torque contest. -- -- Do NOT "fix" this by skipping the front motor: the motor hinge is the -- only joint attaching Wheel_F* to Knuckle_F*, so omitting it leaves the -- front wheels physically detached (verified, tools/diag_frontjoint.luau) -- -- the servo then turns freely because it carries no load, and the car -- generates no front lateral force at all. local motor = Instance.new("HingeConstraint") motor.Name = Chassis.WHEEL_MOTOR_NAME motor.Attachment0 = parentAtt motor.Attachment1 = wheelAtt motor.ActuatorType = Enum.ActuatorType.Motor motor.MotorMaxTorque = 0 -- freewheel until the ChassisController takes over motor.AngularVelocity = 0 motor.Parent = wheelParent end -- Build the full constraint-chassis car model. local function buildCarModel(car: Car, tuning: Tuning): Model local model = Instance.new("Model") model.Name = "Car_" .. car.Id local origin = CFrame.new() -- built at origin; PivotTo() places it later -- Body (PrimaryPart / mesh anchor point) ------------------------------------ local body = makePart({ Name = "Body", Size = Chassis.BODY_SIZE, CFrame = origin, Color = Color3.new(car.Color[1], car.Color[2], car.Color[3]), CanCollide = true, Parent = model, }) -- Ballast: dense low block welded under the body — drops the centre of mass -- so the car corners flat instead of rolling over. local ballast = makePart({ Name = "Ballast", Size = Vector3.new(4, 0.4, 8), CFrame = origin * CFrame.new(0, -0.9, 0), CanCollide = false, Parent = model, }) ballast.Transparency = 1 ballast.CustomPhysicalProperties = PhysicalProperties.new(8, 0.3, 0.5, 1, 1) local ballastWeld = Instance.new("WeldConstraint") ballastWeld.Part0 = body ballastWeld.Part1 = ballast ballastWeld.Parent = body -- Visual bodywork (nose, glass, spoilers, lights, underglow, rims) is added -- by CarBody.build() after assembly. All CarBody parts are Massless, so -- they never alter the chassis physics. -- Seat: input device only (Throttle/Steer). It drives nothing directly. ----- local seat = Instance.new("VehicleSeat") seat.Name = Chassis.SEAT_NAME seat.Size = Vector3.new(4, 0.5, 3) seat.MaxSpeed = 0 -- neutralise built-in drive; constraints do the work seat.Torque = 0 seat.TurnSpeed = 0 seat.HeadsUpDisplay = false seat.CanCollide = false seat.Color = Color3.new(0.1, 0.1, 0.1) -- Seat pan on the CABIN FLOOR, measured up from the ground plane the wheels -- rest on -- NOT up from the body slab. The slab sits at the car's vertical -- middle, so the old "slab top + 0.3" placement put the driver's hips at the -- car's waistline and its head through the roof. Sitting the avatar this low -- means its torso now overlaps the slab, which is why the driver goes in its -- own collision group (see DRIVER_GROUP). seat.CFrame = origin * CFrame.new(0, Chassis.GROUND_OFFSET + Chassis.SEAT_ABOVE_GROUND + seat.Size.Y / 2, 0.2) seat.Parent = model local seatWeld = Instance.new("WeldConstraint") seatWeld.Part0 = body seatWeld.Part1 = seat seatWeld.Parent = body -- Four suspension corners --------------------------------------------------- for _, xSign in ipairs({ -1, 1 }) do for _, zSign in ipairs({ -1, 1 }) do buildCorner(model, body, tuning, xSign, zSign) end end -- Visual shell. If a real imported mesh exists under -- ReplicatedStorage/CarModels/, use it; otherwise fall back to the -- procedural CarBody kit. Meshes are visual-ONLY: every part is Massless, -- CanCollide=false and welded to Body, so the constraint chassis (springs, -- steering servos, motor wheels) and therefore the driving feel are -- completely unchanged no matter which shell is used. local shellTemplate = ReplicatedStorage:FindFirstChild("CarModels") shellTemplate = shellTemplate and shellTemplate:FindFirstChild(car.Id) if shellTemplate then local shell = shellTemplate:Clone() shell.Name = "Shell" -- Measure the shell from its PARTS, not from Model:GetExtentsSize() / -- Model:GetBoundingBox(). Both of those return an axis-aligned box that -- is inflated by rotated parts: silv_s15 reports H=12.32 when its real -- geometry is only 4.71 tall, which made it hover ~4 studs in the air. -- Accumulating each part's own rotated half-extents gives the true box. local function trueAABB(model) local lo = Vector3.new(math.huge, math.huge, math.huge) local hi = Vector3.new(-math.huge, -math.huge, -math.huge) local found = false for _, p in ipairs(model:GetDescendants()) do if p:IsA("BasePart") then found = true local cf, s = p.CFrame, p.Size local hx = 0.5 * ( math.abs(cf.RightVector.X * s.X) + math.abs(cf.UpVector.X * s.Y) + math.abs(cf.LookVector.X * s.Z) ) local hy = 0.5 * ( math.abs(cf.RightVector.Y * s.X) + math.abs(cf.UpVector.Y * s.Y) + math.abs(cf.LookVector.Y * s.Z) ) local hz = 0.5 * ( math.abs(cf.RightVector.Z * s.X) + math.abs(cf.UpVector.Z * s.Y) + math.abs(cf.LookVector.Z * s.Z) ) local c = p.Position lo = Vector3.new(math.min(lo.X, c.X - hx), math.min(lo.Y, c.Y - hy), math.min(lo.Z, c.Z - hz)) hi = Vector3.new(math.max(hi.X, c.X + hx), math.max(hi.Y, c.Y + hy), math.max(hi.Z, c.Z + hz)) end end if not found then return nil, nil, nil end return lo, hi, hi - lo end -- 1. NORMALISE SIZE. Library meshes range 15.6 -> 19.0 studs long at -- native scale, so cars looked inconsistent and the short ones had no -- cabin room for the avatar. Uniform Model:ScaleTo keeps each mesh's -- own proportions while making every car the same length. -- The long axis is NOT always Z: gtr_r34 and hyper_x are modelled -- rotated 90 degrees (length on X), so scaling by .Z blew them up to -- ~2x. Use whichever horizontal axis is actually longer. local _, _, rawSize = trueAABB(shell) if rawSize then local rawLen = math.max(rawSize.X, rawSize.Z) if rawLen > 0.01 then shell:ScaleTo(Chassis.SHELL_LENGTH / rawLen) end end -- 2. SIT IT ON THE GROUND. Align the true box's BOTTOM to the ground -- plane the wheels rest on, then sink by SHELL_DROP so tyres aren't -- swallowed. Measured against the pivot, since PivotTo moves the pivot. local lo2 = trueAABB(shell) local bottomOffset = 0 if lo2 then bottomOffset = lo2.Y - shell:GetPivot().Position.Y end -- 2b. FACE THE SHELL FORWARD. PivotTo below forces the shell's pivot -- orientation to the body's, so a shell inherits whatever yaw its -- author happened to save it with -- there is no automatic -- alignment. Three of the five library shells were modelled -- nose-toward -Z (the chassis convention, since the front axle is -- built at Z = -WHEELBASE/2) and looked right by luck; hatch_junk -- and hyper_x were modelled nose-toward +Z and rendered BACKWARDS -- on a correctly-built chassis. -- Derive each shell's true forward from its OWN wheel names rather -- than a hand-maintained per-car constant: F* wheels are the front -- axle, R* the rear, so (front - rear) is the nose direction in the -- shell's local frame. Solve the yaw that maps it onto the chassis -- forward axis. This also covers 90-degree-rotated shells, and -- self-corrects for any shell added later. If the cue is missing or -- degenerate, rotate nothing (fail soft) rather than guess. -- -- HISTORY: a SHELL_YAW_OVERRIDE table used to pin hatch_junk and -- hyper_x to yaw 0, on the theory that their wheel labels were -- reversed. That was WRONG and has been removed. Measurement -- (tools/diag_shellyaw.luau) showed shell-vs-body yaw was already -- 0.0 degrees on those cars -- there was no shell rotation error at -- all. The real defect was the chassis forward convention being -- inverted against the VehicleSeat (see buildCorner, isFront), which -- made every car drive out of its labelled rear. With that fixed, -- an override forcing yaw 0 would REINTRODUCE a 180-degree flip. -- Do not re-add per-car yaw constants without first running -- diag_shellyaw.luau and showing a non-zero yawDelta. local shellYaw = 0 local wheelsFolder = shell:FindFirstChild("Wheels") if wheelsFolder then local pivotInv = shell:GetPivot():Inverse() local fSum, fN = Vector3.zero, 0 local rSum, rN = Vector3.zero, 0 for _, w in ipairs(wheelsFolder:GetChildren()) do -- Use the group's CENTROID, not an arbitrary first BasePart: -- FindFirstChildWhichIsA can return a rotor or caliper that -- sits up to ~1 stud off the wheel centre (measured, see the -- root-pick note in the wheel re-hang below). local sum, n = Vector3.zero, 0 if w:IsA("BasePart") then sum, n = w.Position, 1 end for _, d in ipairs(w:GetDescendants()) do if d:IsA("BasePart") then sum, n = sum + d.Position, n + 1 end end if n > 0 then local head = w.Name:sub(1, 1):upper() local localPos = pivotInv * (sum / n) if head == "F" then fSum, fN = fSum + localPos, fN + 1 elseif head == "R" then rSum, rN = rSum + localPos, rN + 1 end end end if fN > 0 and rN > 0 then local nose = (fSum / fN) - (rSum / rN) -- Horizontal component only; a shell sitting nose-down must not -- tilt the whole body. if math.abs(nose.X) + math.abs(nose.Z) > 0.05 then -- Map the shell's nose onto chassis forward, which is -Z -- (see the forward-convention note in buildCorner). shellYaw = -math.atan2(nose.X, -nose.Z) end end end -- Rotating about the pivot swings the bodywork sideways whenever the -- pivot is not the geometric centre (hyper_x's centre sits 0.70 studs -- off it, so a 180-degree flip would shift the shell 1.4 studs off the -- chassis). Cancel that by re-centring the shell's horizontal AABB -- centre onto the body axis; Y is untouched, so the ground alignment -- solved above still holds (a yaw rotation cannot change world Y -- extents). local hi2 = select(2, trueAABB(shell)) local centreFix = CFrame.new() if lo2 and hi2 then local centreLocal = shell:GetPivot():Inverse() * ((lo2 + hi2) / 2) centreFix = CFrame.new(-centreLocal.X, 0, -centreLocal.Z) end shell:PivotTo( body.CFrame * CFrame.new(0, Chassis.GROUND_OFFSET - bottomOffset - SHELL_DROP, 0) * CFrame.Angles(0, shellYaw, 0) * centreFix ) -- 3. DROP THE SEAT TO FIT THIS SHELL'S ROOF. Cabin height varies by more -- than a stud across the library (hyper_x roof sits 0.72 above the -- body plane, gtr_r34 1.80). A single fixed seat height that looks -- right in the tallest car pushes the driver's head through the -- roof of the lowest one, so solve for the seat from the measured -- roofline instead: seat = roof - head height - clearance. local seatPart = model:FindFirstChild(Chassis.SEAT_NAME) if seatPart and rawSize then -- The roofline is NOT the top of the shell's bounding box. Every -- library mesh carries junk above the cabin -- gtr_r34 has a stray -- part literally named "Brick" 0.74 studs above its roof, rx7_fc -- 0.51 -- so measuring to the AABB top invents headroom that isn't -- there and the head pokes through the actual roof. Take the top of -- the largest part by volume instead: that is the main body mesh. local lo3, hi3 = trueAABB(shell) local roofY = hi3 and hi3.Y or nil local bestVol = -1 for _, p in ipairs(shell:GetDescendants()) do if p:IsA("BasePart") then local vol = p.Size.X * p.Size.Y * p.Size.Z if vol > bestVol then bestVol = vol local cf, sz = p.CFrame, p.Size roofY = p.Position.Y + 0.5 * (math.abs(cf.RightVector.Y * sz.X) + math.abs(cf.UpVector.Y * sz.Y) + math.abs( cf.LookVector.Y * sz.Z )) end end end if roofY and lo3 then local roofLocal = roofY - body.Position.Y -- HEAD_ABOVE_SEAT was measured from the seat part's CENTRE, but -- seatLocal below is the pan height (the part is then placed at -- seatLocal + Size.Y/2). Subtract the half-thickness here or every -- car quietly loses that much of its intended clearance -- which -- is why hyper_x measured 0.07 instead of the requested 0.30. local wanted = roofLocal - Chassis.HEAD_ABOVE_SEAT - Chassis.ROOF_CLEARANCE - seatPart.Size.Y / 2 -- Allow sinking to the cabin floor: a low roof must win over the -- nominal seat height, otherwise the clamp silently discards the -- whole adaptive solve (which is what used to happen). The shell -- bottom sits SHELL_DROP below the ground plane, so the seat can -- go slightly under GROUND_OFFSET without breaching the underside -- -- hyper_x (lowest roof) needs that headroom. local floor = Chassis.GROUND_OFFSET - SHELL_DROP + 0.1 local seatLocal = math.clamp(wanted, floor, Chassis.GROUND_OFFSET + Chassis.SEAT_ABOVE_GROUND) seatPart.CFrame = body.CFrame * CFrame.new(0, seatLocal + seatPart.Size.Y / 2, 0.2) -- A WeldConstraint locks in the relative offset it had when it -- was created, so moving the seat afterwards would be snapped -- straight back. Rebuild the weld at the new offset. for _, w in ipairs(body:GetChildren()) do if w:IsA("WeldConstraint") and (w.Part1 == seatPart or w.Part0 == seatPart) then w:Destroy() end end local seatWeld2 = Instance.new("WeldConstraint") seatWeld2.Part0 = body seatWeld2.Part1 = seatPart seatWeld2.Parent = body end end local shellRoot = shell.PrimaryPart or shell:FindFirstChildWhichIsA("BasePart", true) for _, d in ipairs(shell:GetDescendants()) do if d:IsA("BasePart") then d.Anchored = false d.CanCollide = false d.CanTouch = false d.CanQuery = false d.Massless = true end end if shellRoot then local w = Instance.new("WeldConstraint") w.Part0 = body w.Part1 = shellRoot w.Parent = body end shell.Parent = model -- Hide the placeholder blocks the mesh replaces. body.Transparency = 1 -- ...INCLUDING the physics wheels. The shell brings its own modelled -- wheels, so leaving the chassis cylinders opaque puts TWO sets of -- wheels on the car -- the mesh tyre plus a grey rubber cylinder over -- it. That is what reads as absurdly wide tyres. They aren't even -- concentric: the cylinder's outer edge sits at X=4.10 against the -- gtr_r34 mesh tyre's 4.52, so the duplicate juts out past the real -- wheel at a different track width. -- This is deliberately INSIDE the shell branch. The procedural CarBody -- fallback (else-branch) has no wheels of its own -- buildRims only -- welds a small Rim disc INTO each Wheel_ part, at 1.2x radius versus -- the wheel's 2x, and transparency is not inherited by children. Hide -- these unconditionally and a shell-less car visibly rides on four -- undersized floating discs. for _, d in ipairs(model:GetChildren()) do if d:IsA("BasePart") and d.Name:match("^Wheel_") then d.Transparency = 1 end end -- RE-HANG THE MODELLED WHEELS ON THE PHYSICS WHEELS. The imported -- shells weld every wheel part straight to the body mesh (the template -- weld graph is MB <-> FL, MB <-> Rotor, MB <-> Caliper, ...), so the -- visible tyres are rigidly glued to the chassis: their assembly root -- comes out as DriveSeat rather than the wheel. They never spin and -- never steer -- painted-on hubcaps, which is most of why the wheels -- looked wrong in motion. Cut those welds and re-weld each wheel group -- to its own Wheel_ cylinder so it inherits that cylinder's spin, and -- on the front axle its steer. local wheelGroups = shell:FindFirstChild("Wheels") if wheelGroups then for _, w in ipairs(wheelGroups:GetChildren()) do local phys = model:FindFirstChild("Wheel_" .. w.Name:upper()) local group, inGroup = {}, {} if w:IsA("BasePart") then group[#group + 1] = w inGroup[w] = true end for _, d in ipairs(w:GetDescendants()) do if d:IsA("BasePart") then group[#group + 1] = d inGroup[d] = true end end -- ROOT MUST BE THE TYRE, so pick the largest-volume part in the -- group. This previously used FindFirstChildWhichIsA, i.e. an -- ARBITRARY first BasePart -- on the 18-part wheel groups that -- lands on a rotor/caliper offset from the wheel centre. The -- group is then translated to make root concentric with the -- axle (see delta below), so a bad root displaces the visible -- tyre by exactly that offset, which reads as wheels sticking -- out past the bodywork. Measured root-to-centre offsets on the -- templates (tools/diag_wheelroot2.luau): rx7_fc 0.955, -- silv_s15 0.988, gtr_r34 0.285-0.310, hatch_junk 0.000-0.035 -- -- matching the report that the RX-7 was the obvious one. local root, bestVol = nil, -1 for _, p in ipairs(group) do local vol = p.Size.X * p.Size.Y * p.Size.Z if vol > bestVol then root, bestVol = p, vol end end if phys and root then -- Drop only the welds that tie this group to the OUTSIDE -- (i.e. to the bodywork). Welds wholly inside the group are -- the wheel's own construction and must survive. for _, c in ipairs(shell:GetDescendants()) do if c:IsA("WeldConstraint") or c:IsA("Weld") then local a, b = c.Part0, c.Part1 local aIn, bIn = a ~= nil and inGroup[a], b ~= nil and inGroup[b] if (aIn or bIn) and not (aIn and bIn) then c:Destroy() end end end -- Make it CONCENTRIC with the cylinder it now rides on. -- Each shell models its own track and wheelbase (the -- gtr_r34 rear tyre sits 1.34 studs ahead of the chassis -- axle, the fronts 0.33 inboard). A weld preserves relative -- offset, so welding at that offset would make the tyre -- ORBIT the axle at 1.34 studs radius instead of spinning -- on it -- far worse than the static wheel it replaces. local delta = phys.Position - root.Position for _, p in ipairs(group) do p.CFrame = p.CFrame + delta end -- The template hung every part off the body, so with those -- welds cut the group has no internal rigidity left of its -- own. Rebuild it around root (these parts are already -- Massless + CanCollide=false, so this adds no physics -- cost), then hang root off the cylinder. for _, p in ipairs(group) do if p ~= root then local iw = Instance.new("WeldConstraint") iw.Part0 = root iw.Part1 = p iw.Parent = root end end local pw = Instance.new("WeldConstraint") pw.Part0 = phys pw.Part1 = root pw.Parent = phys end end end local cabin = model:FindFirstChild("Cabin") if cabin then cabin:Destroy() end else -- Sculpted visual body kit (per-car styling; all parts Massless + welded) CarBody.build(model, body, car) end -- Wheels must not collide with their own chassis (see ensureCollisionGroups). ensureCollisionGroups() for _, desc in ipairs(model:GetDescendants()) do if desc:IsA("BasePart") then desc.CollisionGroup = if desc.Name:match("^Wheel_") then WHEEL_GROUP else CHASSIS_GROUP end end model.PrimaryPart = body return model end -- Find a reasonable spawn CFrame (a SpawnPad in the map, else origin) -------- -- CharacterAdded can fire before MapBuilder.build() has finished, in which case -- a naive lookup silently falls back to the origin and dumps the car ~8,600 -- studs off-circuit. Wait briefly for the pad rather than guessing. local function spawnCFrame(): CFrame local deadline = os.clock() + 10 repeat local map = Workspace:FindFirstChild("Map") local pad = map and map:FindFirstChild("SpawnPad", true) if pad and pad:IsA("BasePart") then return pad.CFrame * CFrame.new(0, 5, 0) end task.wait(0.1) until os.clock() > deadline warn("[CarManager] SpawnPad never appeared; falling back to origin.") return CFrame.new(0, 8, 0) end --- Despawn the player's current car if any. function CarManager.despawn(player: Player) local existing = activeCars[player] if existing then existing:Destroy() activeCars[player] = nil end end --- Spawn (or replace) the player's equipped car and seat them in it. function CarManager.spawn(player: Player, carId: string?) local data = DataManager.get(player) if not data then return end local id = carId or data.EquippedCarId local car = Cars.Get(id) if not car then return end -- Restricted car (RX-7) can only be driven by allowlisted UserIds. if not GameConfig.canDrive(player.UserId, car.Id, car.Restricted) then return end CarManager.despawn(player) local tuning = Chassis.tuningFor(car) local model = buildCarModel(car, tuning) local target = spawnCFrame() model.Parent = carsFolder -- Pivot AFTER parenting: pivoting an unparented model then reparenting lets -- the physics solver re-resolve from the model's build origin, which drops -- the car back at (0,0,0) instead of the spawn pad. model:PivotTo(target) model:SetAttribute("OwnerUserId", player.UserId) model:SetAttribute("CarId", id) -- Record every movable part's offset from the body while the model is -- guaranteed intact (freshly built, nothing has simulated yet), and hold -- SERVER authority for now. Handing ownership to the client before the -- driver is seated lets the client re-assert a stale layout on top of us. local primary = model.PrimaryPart local offsets: { [BasePart]: CFrame } = {} local movable: { BasePart } = {} for _, desc in ipairs(model:GetDescendants()) do if desc:IsA("BasePart") and not desc.Anchored then if primary then offsets[desc] = primary.CFrame:Inverse() * desc.CFrame end table.insert(movable, desc) pcall(function() desc:SetNetworkOwner(nil) end) end end local character = player.Character local seat = model:FindFirstChild(Chassis.SEAT_NAME) :: VehicleSeat? if character and seat and primary then -- Why the car is moved to the player instead of the player to the car: -- -- The character is CLIENT-owned, so a server-side PivotTo of it does -- NOT stick -- it holds for one frame, then the client snaps it back. -- 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 (constraints don't merge them the -- way welds do), only the body follows. The rest stay at the pad and -- the car is torn apart -- measured ~15,700 studs -- then falls out of -- the world. -- -- So: never make Sit() do the travelling. Teleport the INTACT car to -- wherever the character already is, part-by-part through the recorded -- offsets (PivotTo alone won't carry separate assemblies reliably), -- and only then seat the driver. Sit() now moves nobody and nothing -- tears. Ownership goes to the client last, once the rig is settled. local humanoid = character:FindFirstChildOfClass("Humanoid") local root = character:FindFirstChild("HumanoidRootPart") :: BasePart? if humanoid and root then -- Face the car the way the character is facing, forced level so it -- can never spawn on its roof. local look = root.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 origin = root.Position + Vector3.new(0, SEAT_LIFT, 0) local base = CFrame.lookAt(origin, origin + flat.Unit) for _, part in ipairs(movable) do part.CFrame = base * offsets[part] part.AssemblyLinearVelocity = Vector3.zero part.AssemblyAngularVelocity = Vector3.zero end AntiCheat.grant(player) -- Driver ignores the chassis BEFORE sitting: the avatar is seated -- down inside the body slab, and overlapping colliders at Sit() is -- what launches characters. setCharacterGroup(character, DRIVER_GROUP) seat:Sit(humanoid) -- Restore normal collision when they get out, otherwise the player -- walks around permanently unable to touch cars. local seatConn: RBXScriptConnection seatConn = seat:GetPropertyChangedSignal("Occupant"):Connect(function() if seat.Occupant == nil then setCharacterGroup(character, "Default") seatConn:Disconnect() end end) task.defer(function() task.wait(SETTLE_TIME) -- let Sit() + the solver settle if not primary:IsDescendantOf(Workspace) then return end 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 activeCars[player] = model end function CarManager.init() carsFolder = Instance.new("Folder") carsFolder.Name = "ActiveCars" carsFolder.Parent = Workspace -- FLIP RECOVERY: a car that ends up on its roof has no way to right itself, -- so the player is stranded upside down indefinitely -- endurance testing -- left one inverted (up = -0.6) grinding a barrier for 140 seconds. When a -- car is inverted AND effectively stationary for a few consecutive checks -- (so we never flip someone mid-drift), lift it clear and set it upright, -- preserving heading. Ownership is pulled back to the server for the -- teleport: PivotTo on client-owned parts gets reverted by the owning -- client on the next physics frame. local flipTicks: { [Player]: number } = {} task.spawn(function() while true do task.wait(1) for _, player in ipairs(Players:GetPlayers()) do local ok, err = pcall(function() local model = activeCars[player] if not model or not model.Parent then flipTicks[player] = nil return end local body = model:FindFirstChild("Body") local root = (body and body:IsA("BasePart") and body) or model.PrimaryPart if not root then return end local upright = root.CFrame.UpVector:Dot(Vector3.yAxis) local slow = root.AssemblyLinearVelocity.Magnitude < 8 if upright >= 0.35 or not slow then flipTicks[player] = 0 return end flipTicks[player] = (flipTicks[player] or 0) + 1 if flipTicks[player] < 3 then return end flipTicks[player] = 0 local parts: { BasePart } = {} for _, d in ipairs(model:GetDescendants()) do if d:IsA("BasePart") and not d.Anchored then table.insert(parts, d) end end for _, d in ipairs(parts) do pcall(function() d:SetNetworkOwner(nil) end) end local look = root.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 target = root.Position + Vector3.new(0, 6, 0) model:PivotTo(CFrame.lookAt(target, target + flat.Unit)) for _, d in ipairs(parts) do d.AssemblyLinearVelocity = Vector3.zero d.AssemblyAngularVelocity = Vector3.zero end AntiCheat.grant(player, 3) task.wait(0.3) for _, d in ipairs(parts) do pcall(function() d:SetNetworkOwner(player) end) end warn(("[CarManager] righted flipped car for %s"):format(player.Name)) end) if not ok then warn("[CarManager] flip-recovery iteration failed: " .. tostring(err)) end end end end) Players.PlayerRemoving:Connect(function(player) CarManager.despawn(player) end) -- VOID RECOVERY: nothing else rescues a car that leaves the world. If it -- falls off the map it drops past Workspace.FallenPartsDestroyHeight, the -- model is deleted out from under the driver, and the player is left -- standing (or seatless) with no car and no way to get another -- observed -- in endurance testing as 100+ seconds of a dead session. Watch for a car -- that has fallen below the recovery floor OR whose model was destroyed -- while the player is still in the game, and respawn it on the pad. local RECOVER_Y = Workspace.FallenPartsDestroyHeight + 100 task.spawn(function() while true do task.wait(1) for _, player in ipairs(Players:GetPlayers()) do local ok, err = pcall(function() if not player.Parent or not player.Character then return end local model = activeCars[player] if not model then return end local gone = model.Parent == nil local fell = false if not gone then local body = model:FindFirstChild("Body") local root = (body and body:IsA("BasePart") and body) or model.PrimaryPart fell = root ~= nil and root.Position.Y < RECOVER_Y end if gone or fell then warn( ("[CarManager] recovering car for %s (destroyed=%s fell=%s)"):format( player.Name, tostring(gone), tostring(fell) ) ) -- CRITICAL: spawn() teleports the car to wherever the -- CHARACTER is. During a void fall the seated character is -- falling too, so respawning here would drop a fresh car -- into the void and loop forever. Put the character back on -- solid ground FIRST, then build the car around it. local character = player.Character local root = character and character:FindFirstChild("HumanoidRootPart") if root and root:IsA("BasePart") then local safe = spawnCFrame() AntiCheat.grant(player, 5) root.CFrame = safe + Vector3.new(0, 5, 0) root.AssemblyLinearVelocity = Vector3.zero root.AssemblyAngularVelocity = Vector3.zero task.wait(0.2) end local data = DataManager.get(player) CarManager.spawn(player, data and data.EquippedCarId) end end) if not ok then warn("[CarManager] void-recovery iteration failed: " .. tostring(err)) end end end end) end -- The player's live car model, or nil if they have none right now. function CarManager.getCar(player: Player): Model? local model = activeCars[player] if model and model.Parent then return model end return nil end -- The physics root of the player's car (the chassis Body). ModeManager samples -- this to measure real, server-side position — never client-reported. function CarManager.getRoot(player: Player): BasePart? local model = CarManager.getCar(player) if not model then return nil end local body = model:FindFirstChild("Body") if body and body:IsA("BasePart") then return body end return model.PrimaryPart end -- Test seam: build a bare chassis with no player, seat or network ownership. -- Used by tools/diag_steering.luau to measure handling headlessly. Not part of -- the game path -- CarManager.spawn is the only way a player ever gets a car. CarManager._buildForDiag = buildCarModel return CarManager