From f70a0b06bba30dfff0553d8e0ec338e174b84451 Mon Sep 17 00:00:00 2001 From: "R3AL.CH1CK3N" Date: Fri, 31 Jul 2026 16:15:55 -0400 Subject: [PATCH] fix(client): stop the chassis watchdog dying on a nil seat during respawn The stale-loop check indexed seatPart without a nil guard: not seatPart:IsDescendantOf(workspace) On respawn CarManager destroys the old car, so humanoid.SeatPart is nil for a frame while heartbeatConn is still live. That path then indexed a nil value, the error propagated out of the task.spawn thread, and the watchdog - the only self-healing path in the controller - DIED PERMANENTLY. The car was undrivable from then on, and the bug looked intermittent because the edge-triggered SeatPart signal still covered the simple nil -> seat transition. Nil-check seatPart before indexing, and wrap the whole watchdog body in pcall so no future error can ever silently kill the loop again (warn instead). Verified: 5 consecutive respawns then drives to MaxSpeed 163 studs/s at road level (up=1.00), TIME_ATTACK sessionKm advances 0.003 -> 0.169 km. --- src/client/ChassisController.luau | 49 +++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/src/client/ChassisController.luau b/src/client/ChassisController.luau index 2b6a5ae..965eeab 100644 --- a/src/client/ChassisController.luau +++ b/src/client/ChassisController.luau @@ -144,22 +144,39 @@ function ChassisController.init() -- if we are seated in a DriveSeat but have no live loop, arm it. task.spawn(function() while humanoid.Parent do - local seatPart = humanoid.SeatPart - local seated = seatPart - and seatPart:IsA("VehicleSeat") - and seatPart.Name == Chassis.SEAT_NAME - -- 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() + -- The whole body is pcall-wrapped: this watchdog is the ONLY + -- self-healing path. If it ever errors the thread dies silently + -- and the car becomes permanently undrivable, with the failure + -- appearing intermittent because the edge-triggered seat signal + -- still handles the easy nil -> seat case. Never let it die. + local ok, err = pcall(function() + local seatPart = humanoid.SeatPart + local seated = seatPart ~= nil + and seatPart:IsA("VehicleSeat") + and seatPart.Name == Chassis.SEAT_NAME + -- 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 sitting in, or it left the world. + -- NOTE: seatPart is nil for a frame whenever the old car is + -- destroyed on respawn, so it MUST be nil-checked before it + -- is indexed -- that crash is what killed this loop before. + local stale = heartbeatConn ~= nil + and ( + drivingSeat ~= seatPart + or seatPart == nil + or not seatPart:IsDescendantOf(workspace) + ) + if seated and (not heartbeatConn or stale) then + stopDriving() + startDriving(seatPart :: VehicleSeat) + elseif not seated and heartbeatConn then + stopDriving() + end + end) + if not ok then + warn("[ChassisController] watchdog iteration failed: " .. tostring(err)) end task.wait(0.5) end