--[[ diag_shelllabels.luau -- compare the RAW shell assets, before any code touches them. Purpose: find what is different about hatch_junk and hyper_x (reported backwards) versus rx7_fc / silv_s15 / gtr_r34 (reported fine). A convention error would hit all five equally, so the defect must be per-asset. The shell yaw solver derives "nose" from the F*/R* wheel NAMES, so if those two shells label their axles opposite to the others, the solver is fed a reversed cue and dutifully rotates them 180 degrees. IMPORTANT -- this reads the stored shell assets directly. It does NOT call require() on CarManager, and it does NOT rely on any value derived from a constant. Every number below is read off the asset's own geometry, so it genuinely differs between a correctly-labelled and a mislabelled shell. Independent cue: cars are longer than they are wide, so the shell's LONG horizontal axis is the longitudinal (nose-tail) axis. Comparing the wheel label cue against the bounding-box long axis tells us whether the labels agree with the geometry -- without trusting either one alone. ]] local RS = game:GetService("ReplicatedStorage") local SSS = game:GetService("ServerScriptService") local out = {} local function log(...) local parts = {} for i = 1, select("#", ...) do parts[#parts + 1] = tostring((select(i, ...))) end table.insert(out, table.concat(parts, " ")) end -- Locate the folder holding the shell models. Search broadly rather than -- assuming a path, and report what was found so a wrong guess is visible. local function findShellRoot() for _, root in ipairs({ RS, SSS, workspace, game:GetService("ServerStorage") }) do for _, d in ipairs(root:GetDescendants()) do if d:IsA("Folder") and (d.Name == "Shells" or d.Name == "CarShells" or d.Name == "Bodies") then return d end end end return nil end local shellRoot = findShellRoot() if not shellRoot then -- Fall back: find any Model that contains a "Wheels" child. local found = {} for _, root in ipairs({ RS, SSS, workspace, game:GetService("ServerStorage") }) do for _, d in ipairs(root:GetDescendants()) do if d:IsA("Model") and d:FindFirstChild("Wheels") then table.insert(found, d) end end end if #found == 0 then return "FATAL: no shell models found (no Shells folder, no Model with a Wheels child)" end log("no Shells folder; found shells by Wheels child:") shellRoot = nil local list = {} for _, m in ipairs(found) do table.insert(list, m) end -- Reuse the same reporting path below. shellRoot = { GetChildren = function() return list end, } end log( string.format( "%-11s %-26s %-22s %-18s %s", "shell", "F centroid (local)", "R centroid", "long axis", "labels agree?" ) ) log(string.rep("-", 100)) local shells = shellRoot:GetChildren() local reported = 0 for _, shell in ipairs(shells) do if not shell:IsA("Model") then continue end local wheels = shell:FindFirstChild("Wheels") if not wheels then continue end local pivotInv = shell:GetPivot():Inverse() -- Centroid of each labelled axle, in the shell's own local frame. local fSum, fN = Vector3.zero, 0 local rSum, rN = Vector3.zero, 0 for _, w in ipairs(wheels:GetChildren()) do local sum, n = Vector3.zero, 0 if w:IsA("BasePart") then sum, n = w.Position, 1 end for _, d in ipairs(w:GetDescendants()) do if d:IsA("BasePart") then sum, n = sum + d.Position, n + 1 end end if n > 0 then local head = w.Name:sub(1, 1):upper() local localPos = pivotInv * (sum / n) if head == "F" then fSum, fN = fSum + localPos, fN + 1 elseif head == "R" then rSum, rN = rSum + localPos, rN + 1 end end end if fN == 0 or rN == 0 then log(string.format("%-11s NO F/R WHEEL LABELS (F=%d R=%d)", shell.Name, fN, rN)) reported += 1 continue end local fc = fSum / fN local rc = rSum / rN local nose = fc - rc -- Independent geometric cue: the shell's longest horizontal extent is the -- nose-tail axis. If |nose.Z| dominates, labels lie along Z (expected). local minV, maxV for _, d in ipairs(shell:GetDescendants()) do if d:IsA("BasePart") then local p = pivotInv * d.Position minV = minV and Vector3.new(math.min(minV.X, p.X), 0, math.min(minV.Z, p.Z)) or Vector3.new(p.X, 0, p.Z) maxV = maxV and Vector3.new(math.max(maxV.X, p.X), 0, math.max(maxV.Z, p.Z)) or Vector3.new(p.X, 0, p.Z) end end local spanX = maxV and (maxV.X - minV.X) or 0 local spanZ = maxV and (maxV.Z - minV.Z) or 0 local longAxis = (spanZ >= spanX) and "Z" or "X" -- The convention is: forward = -Z. So a correctly-labelled shell has its -- FRONT axle at a MORE NEGATIVE local Z than its rear, i.e. nose.Z < 0. local agrees = nose.Z < 0 local verdict if math.abs(nose.Z) < 0.05 then verdict = "DEGENERATE (no Z separation)" elseif agrees then verdict = "OK (F is toward -Z)" else verdict = "*** REVERSED (F is toward +Z) ***" end log( string.format( "%-11s F(%+.1f,%+.1f) %-11s R(%+.1f,%+.1f) %-6s %s(x%.0f z%.0f) %s", shell.Name, fc.X, fc.Z, "", rc.X, rc.Z, "", longAxis, spanX, spanZ, verdict ) ) reported += 1 end log("") if reported == 0 then log(">>> NOTHING INSPECTED -- no shells with a Wheels child. This is a FAILURE, not a pass.") else log(string.format(">>> inspected %d shells", reported)) log(" Convention: forward = -Z, so a correct shell has F wheels at more NEGATIVE Z than R.") log(" Any shell marked REVERSED has mislabelled wheels -- the yaw solver's cue is inverted") log(" for that car only, which is exactly the per-car symptom reported.") end return table.concat(out, "\n")