Files
tokyo-wangan-racing/src/server/CarManager.luau
T
jesse c8dabea53a Daikoku PA: lift mainline over the apron, rebuild both ramps
The PA ring road and the expressway mainline were both at ground level, so
the ring parapet stood INSIDE the carriageway (measured -10.0 studs of
vertical overlap: a wall in the middle of the road).

Reference photos of the real Daikoku Futo PA show the expressway flying
OVER the parking apron on pillars, with the PA at ground level and ramps
climbing up to meet it. Rebuilt to match:

- mainline lifted 38 studs through the junction, eased in/out (max 5.26%
  grade, within real expressway limits)
- PA ring + apron stay at ground level; legacy PARingPillar props removed
- both on/off ramps regraded over the full rise with smoothstep easing,
  segments overlapped 3 studs so wheels can't catch a seam, plus support
  pillars and edge kerbs where the deck is high

Verified in-engine against built parts (tools/diag_daikoku.luau), not just
source: clearance now +28.0 studs, both ramps continuous (max step 5.62,
no gaps). tools/plot_daikoku.py renders the plan/section for comparison.
2026-08-02 15:56:13 -04:00

1048 lines
42 KiB
Luau

--!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/<carId>, 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