fix(client): re-arm chassis when the drive loop is bound to a stale car
CI / quality (push) Failing after 47s

Respawning left the car undrivable. CarManager.spawn() destroys the old car
and builds a new one, but the client's Heartbeat connection survived and kept
driving the DESTROYED car's motors. Because heartbeatConn stayed non-nil, the
cold-start watchdog concluded the chassis was already armed and never armed
the new car - torque stayed at the idle brake value and the car never moved.

Track the seat the live loop is bound to (drivingSeat) and treat the loop as
stale when that seat isn't the one we're sitting in, or its car has left the
workspace. On stale, stop and re-arm against the new car.

Verified: after 4 respawn cycles the car accelerates 49 -> 163 studs/s,
travels 273 studs, stays upright at road level.
This commit is contained in:
2026-07-31 16:00:06 -04:00
parent 3ae71f9dd3
commit c8809efa7b
+16 -1
View File
@@ -29,6 +29,11 @@ local ChassisController = {}
local heartbeatConn: RBXScriptConnection? = nil
local seatedConn: RBXScriptConnection? = nil
-- The seat the live heartbeatConn is bound to. On respawn the old car is
-- destroyed and a new one spawned; the connection stays alive but drives the
-- DESTROYED car's motors. Tracking the seat lets the watchdog notice that the
-- loop is stale and re-arm against the new car.
local drivingSeat: VehicleSeat? = nil
local function findCarModel(seat: VehicleSeat): Model?
local model = seat:FindFirstAncestorOfClass("Model")
@@ -70,6 +75,7 @@ local function startDriving(seat: VehicleSeat)
return
end
drivingSeat = seat
heartbeatConn = RunService.Heartbeat:Connect(function()
local throttle = seat.ThrottleFloat
local steer = seat.SteerFloat
@@ -106,6 +112,7 @@ local function stopDriving()
heartbeatConn:Disconnect()
heartbeatConn = nil
end
drivingSeat = nil
end
function ChassisController.init()
@@ -141,7 +148,15 @@ function ChassisController.init()
local seated = seatPart
and seatPart:IsA("VehicleSeat")
and seatPart.Name == Chassis.SEAT_NAME
if seated and not heartbeatConn then
-- A live connection bound to a different seat (or to a car that
-- has since been destroyed) is STALE: it keeps driving the old
-- car's motors, so heartbeatConn stays non-nil and the new car
-- never gets armed. Re-arm whenever the bound seat isn't the
-- one we're actually sitting in, or its car left the world.
local stale = heartbeatConn ~= nil
and (drivingSeat ~= seatPart or not seatPart:IsDescendantOf(workspace))
if seated and (not heartbeatConn or stale) then
stopDriving()
startDriving(seatPart :: VehicleSeat)
elseif not seated and heartbeatConn then
stopDriving()