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.
|
-- if we are seated in a DriveSeat but have no live loop, arm it.
|
||||||
task.spawn(function()
|
task.spawn(function()
|
||||||
while humanoid.Parent do
|
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 seatPart = humanoid.SeatPart
|
||||||
local seated = seatPart
|
local seated = seatPart ~= nil
|
||||||
and seatPart:IsA("VehicleSeat")
|
and seatPart:IsA("VehicleSeat")
|
||||||
and seatPart.Name == Chassis.SEAT_NAME
|
and seatPart.Name == Chassis.SEAT_NAME
|
||||||
-- A live connection bound to a different seat (or to a car that
|
-- A live connection bound to a different seat (or to a car
|
||||||
-- has since been destroyed) is STALE: it keeps driving the old
|
-- that has since been destroyed) is STALE: it keeps driving
|
||||||
-- car's motors, so heartbeatConn stays non-nil and the new car
|
-- the old car's motors, so heartbeatConn stays non-nil and
|
||||||
-- never gets armed. Re-arm whenever the bound seat isn't the
|
-- the new car never gets armed. Re-arm whenever the bound
|
||||||
-- one we're actually sitting in, or its car left the world.
|
-- 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
|
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
|
if seated and (not heartbeatConn or stale) then
|
||||||
stopDriving()
|
stopDriving()
|
||||||
startDriving(seatPart :: VehicleSeat)
|
startDriving(seatPart :: VehicleSeat)
|
||||||
elseif not seated and heartbeatConn then
|
elseif not seated and heartbeatConn then
|
||||||
stopDriving()
|
stopDriving()
|
||||||
end
|
end
|
||||||
|
end)
|
||||||
|
if not ok then
|
||||||
|
warn("[ChassisController] watchdog iteration failed: " .. tostring(err))
|
||||||
|
end
|
||||||
task.wait(0.5)
|
task.wait(0.5)
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|||||||
Reference in New Issue
Block a user