fix(client): stop the chassis watchdog dying on a nil seat during respawn
CI / quality (push) Failing after 47s
CI / quality (push) Failing after 47s
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.
This commit is contained in:
@@ -144,23 +144,40 @@ function ChassisController.init()
|
||||
-- if we are seated in a DriveSeat but have no live loop, arm it.
|
||||
task.spawn(function()
|
||||
while humanoid.Parent do
|
||||
-- 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
|
||||
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 actually sitting in, or its car left the world.
|
||||
-- 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 not seatPart:IsDescendantOf(workspace))
|
||||
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
|
||||
end)
|
||||
|
||||
Reference in New Issue
Block a user