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