Daikoku PA: lift mainline over the apron, rebuild both ramps

The PA ring road and the expressway mainline were both at ground level, so
the ring parapet stood INSIDE the carriageway (measured -10.0 studs of
vertical overlap: a wall in the middle of the road).

Reference photos of the real Daikoku Futo PA show the expressway flying
OVER the parking apron on pillars, with the PA at ground level and ramps
climbing up to meet it. Rebuilt to match:

- mainline lifted 38 studs through the junction, eased in/out (max 5.26%
  grade, within real expressway limits)
- PA ring + apron stay at ground level; legacy PARingPillar props removed
- both on/off ramps regraded over the full rise with smoothstep easing,
  segments overlapped 3 studs so wheels can't catch a seam, plus support
  pillars and edge kerbs where the deck is high

Verified in-engine against built parts (tools/diag_daikoku.luau), not just
source: clearance now +28.0 studs, both ramps continuous (max step 5.62,
no gaps). tools/plot_daikoku.py renders the plan/section for comparison.
This commit is contained in:
2026-08-02 15:56:13 -04:00
parent 8808b919c3
commit c8dabea53a
51 changed files with 6558 additions and 65 deletions
+48
View File
@@ -0,0 +1,48 @@
--[[
check_live_source.luau -- did the pushed CarManager source actually land,
and is the running session using it?
diag_steering still reports "found 4 motors" after a push that reported
assembled==expected match=true. Two candidate explanations:
1. the source in the DataModel is stale (push landed elsewhere), or
2. the source is fresh but require() returned a CACHED module instance
from earlier in this Play session, so the old build function is live.
]]
local ServerScriptService = 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
local server = ServerScriptService:FindFirstChild("Server", true)
local mod = server and server:FindFirstChild("CarManager")
log("module:", mod and mod:GetFullName() or "<not found>")
if mod then
local src = mod.Source
log("source bytes:", #src)
log("has REAR-WHEEL DRIVE marker:", tostring(src:find("REAR%-WHEEL DRIVE") ~= nil))
log("has 'if isFront then\\n\\t\\treturn' guard:", tostring(src:find("isFront then\n\t\treturn") ~= nil))
end
-- How many CarManager-like ModuleScripts exist anywhere? A duplicate copy
-- would explain a push landing in the wrong place.
local hits = {}
for _, d in ipairs(game:GetDescendants()) do
if d:IsA("ModuleScript") and d.Name == "CarManager" then
table.insert(hits, d:GetFullName() .. " (" .. #d.Source .. " bytes)")
end
end
log("")
log("all CarManager ModuleScripts in this DataModel:")
for _, h in ipairs(hits) do
log(" " .. h)
end
return table.concat(out, "\n")
Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

+109
View File
@@ -0,0 +1,109 @@
--[[
diag_bootcheck.luau -- decide facing using the BOOT/SPOILER cue, which is
directly interpretable, and verify it survives the build.
Findings so far:
* gtr_r34 template: wheels -10.27, hood -6.22, boot +6.64 -> all three
cues AGREE, nose at -Z. So wheel labels are NOT swapped.
* bodywork "asym" gave gtr_r34 -0.37 while rx7_fc/silv_s15 were
positive, yet all three have boot at +Z. asym is therefore a poor
proxy (it is dominated by whichever big slab a given shell happens to
have) and its verdicts must be discarded.
The boot/spoiler is unambiguous: it belongs at the TAIL. On a correctly
faced car (chassis forward = -Z) the boot must land at POSITIVE body-local
Z. Measure that on the BUILT car, per shell, and compare to the template.
Shells with no named boot get a fallback: the tail is the end furthest
from the front wheels, per that shell's own wheel cue.
]]
local ServerScriptService = game:GetService("ServerScriptService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
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
local Shared = ReplicatedStorage:FindFirstChild("Shared")
local Cars = require(Shared.Cars)
local Chassis = require(Shared.Chassis)
local orig = ServerScriptService:FindFirstChild("Server", true):FindFirstChild("CarManager")
local cm = orig:Clone()
cm.Parent = orig.Parent
local CarManager = require(cm)
local TAIL = { boot = true, spoiler = true, trunk = true, taillight = true, exhaust = true, diffuser = true }
local NOSE = { hood = true, windshield = true, bonnet = true, grille = true, headlight = true, splitter = true }
local function cueZ(root: Instance, frame: CFrame)
local inv = frame:Inverse()
local tS, tN, nS, nN = 0, 0, 0, 0
for _, p in ipairs(root:GetDescendants()) do
if p:IsA("BasePart") then
local nm = p.Name:lower()
local z = (inv * p.Position).Z
if TAIL[nm] then
tS, tN = tS + z, tN + 1
elseif NOSE[nm] then
nS, nN = nS + z, nN + 1
end
end
end
return (tN > 0 and tS / tN or nil), (nN > 0 and nS / nN or nil), tN, nN
end
log("chassis forward = -Z, so TAIL parts must land at POSITIVE body-local Z.")
log("")
log(string.format("%-14s %22s %30s", "car", "TEMPLATE (tail/nose)", "BUILT (tail/nose) verdict"))
log(string.rep("-", 74))
for _, t in ipairs(ReplicatedStorage.CarModels:GetChildren()) do
local id = t.Name
local tT, tNz, tCount, nCount = cueZ(t, t:GetPivot())
local bT, bN = nil, nil
local car = Cars.Get(id)
if car then
local m = CarManager._buildForDiag(car, Chassis.tuningFor(car))
m.Parent = workspace
m:PivotTo(CFrame.new(0, 5010, 0))
for _ = 1, 3 do
RunService.Heartbeat:Wait()
end
local sh = m:FindFirstChild("Shell")
if sh then
bT, bN = cueZ(sh, m.PrimaryPart.CFrame)
end
m:Destroy()
end
local verdict
if bT then
verdict = bT > 0 and "CORRECT" or "*** BACKWARDS ***"
elseif bN then
verdict = bN < 0 and "CORRECT" or "*** BACKWARDS ***"
else
verdict = "no named cue - cannot judge"
end
log(
string.format(
"%-14s tail %8s nose %7s tail %8s nose %7s %s",
id,
tT and string.format("%+.2f", tT) or "n/a",
tNz and string.format("%+.2f", tNz) or "n/a",
bT and string.format("%+.2f", bT) or "n/a",
bN and string.format("%+.2f", bN) or "n/a",
verdict
)
)
end
cm:Destroy()
return table.concat(out, "\n")
+167
View File
@@ -0,0 +1,167 @@
--[[
diag_builtnose.luau -- END-TO-END measurement. Build each car for real via
CarManager._buildForDiag, then measure where the finished shell's nose
actually points relative to the chassis forward axis.
Why this and not more code reading: the solver computes the right 180-degree
yaw (diag_solveryaw) and applies it without centring error (diag_centrefix),
yet two cars still look backwards. So something AFTER the PivotTo must be
undoing it. Rather than guess which line, measure the finished article.
noseDot = (shell nose direction) dot (body LookVector)
+1 -> shell nose points where the car drives (CORRECT)
-1 -> shell nose points backwards (REVERSED)
Cache guard first: require() caches per Studio session, so a stale
CarManager would silently build old code and make this whole test a lie.
]]
local RS = game:GetService("ReplicatedStorage")
local SSS = game:GetService("ServerScriptService")
local out = {}
local function log(s)
table.insert(out, s)
end
local function find(name)
for _, root in ipairs({ SSS, RS, game:GetService("ServerStorage"), workspace }) do
local m = root:FindFirstChild(name, true)
if m and m:IsA("ModuleScript") then
return m
end
end
return nil
end
local cmMod = find("CarManager")
if not cmMod then
return "FATAL: CarManager not found"
end
-- Guard: the reverted source must be what require() hands back.
local srcOK = cmMod.Source:find("isFront = zSign < 0", 1, true) ~= nil
if not srcOK then
return "FATAL: CarManager source is not the reverted version -- aborting"
end
local ok, CM = pcall(require, cmMod)
if not ok then
return "FATAL: require(CarManager) failed: " .. tostring(CM)
end
if type(CM._buildForDiag) ~= "function" then
return "FATAL: CarManager._buildForDiag missing (stale require cache?) -- restart Studio"
end
local Cars = require(RS:WaitForChild("Shared"):WaitForChild("Cars"))
local ChassisMod = require(RS:WaitForChild("Shared"):WaitForChild("Chassis"))
local TARGETS = { "rx7_fc", "silv_s15", "gtr_r34", "hatch_junk", "hyper_x" }
log(string.format("%-11s %-10s %-12s %s", "shell", "noseDot", "yaw err", "verdict"))
log(string.rep("-", 62))
local built = 0
for _, name in ipairs(TARGETS) do
-- buildCarModel(car: Car, tuning: Tuning) -- NOT a string id. Passing a
-- string was the harness bug that produced the CarManager:277 nil error.
local carDef = Cars.Get(name)
if not carDef then
log(string.format("%-11s NOT IN Cars.List", name))
continue
end
local tuning = ChassisMod.tuningFor(carDef)
local okB, car = pcall(CM._buildForDiag, carDef, tuning)
if not okB or not car then
log(string.format("%-11s BUILD FAILED: %s", name, tostring(car)))
continue
end
built += 1
local body = car:FindFirstChild("Body") or car.PrimaryPart
local shell = nil
for _, d in ipairs(car:GetChildren()) do
if d:IsA("Model") and d:FindFirstChild("Wheels") then
shell = d
break
end
end
if not body or not shell then
log(string.format("%-11s no body/shell on built car", name))
car:Destroy()
continue
end
-- Shell nose in WORLD space, from the shell's own wheel labels.
local wf = shell:FindFirstChild("Wheels")
local fSum, fN, rSum, rN = Vector3.zero, 0, Vector3.zero, 0
for _, w in ipairs(wf: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 h = w.Name:sub(1, 1):upper()
if h == "F" then
fSum, fN = fSum + (sum / n), fN + 1
elseif h == "R" then
rSum, rN = rSum + (sum / n), rN + 1
end
end
end
if fN == 0 or rN == 0 then
log(string.format("%-11s no F/R labels on built shell", name))
car:Destroy()
continue
end
local nose = (fSum / fN) - (rSum / rN)
nose = Vector3.new(nose.X, 0, nose.Z)
if nose.Magnitude < 0.01 then
log(string.format("%-11s degenerate nose vector", name))
car:Destroy()
continue
end
nose = nose.Unit
local fwd = body.CFrame.LookVector
fwd = Vector3.new(fwd.X, 0, fwd.Z).Unit
local dot = nose:Dot(fwd)
local yawErr = math.deg(math.acos(math.clamp(dot, -1, 1)))
local verdict
if dot > 0.85 then
verdict = "CORRECT (nose = drive dir)"
elseif dot < -0.85 then
verdict = "*** REVERSED ***"
else
verdict = "skewed"
end
log(
string.format(
"%-11s %-10s %-12s %s",
name,
string.format("%+.3f", dot),
string.format("%.1f deg", yawErr),
verdict
)
)
car:Destroy()
end
log("")
if built == 0 then
log(">>> NOTHING BUILT -- this is a FAILURE, not a pass.")
else
log(string.format(">>> built and measured %d/5 cars, then destroyed them (no scene changes)", built))
log(" This is the finished article, after every stage of the build pipeline.")
end
return table.concat(out, "\n")
+136
View File
@@ -0,0 +1,136 @@
--[[
diag_centrefix.luau -- test whether centreFix is applied in the wrong order.
The build does:
shell:PivotTo(body.CFrame * heightCF * CFrame.Angles(0,yaw,0) * centreFix)
centreFix is a translation derived from the shell's CURRENT (unrotated)
AABB centre. Placing it to the RIGHT of CFrame.Angles means it is expressed
in the ROTATED frame, so a 180-degree yaw negates its X and Z. For the three
shells with yaw ~= 0 this is harmless. For hatch_junk and hyper_x (yaw 180)
it moves the shell by -2 * centreLocal instead of cancelling the offset.
This measures the resulting AABB-centre error for BOTH orderings, per shell.
The numbers can come out either way -- if the current ordering is fine, both
errors are ~0 and the hypothesis is dead.
]]
local RS = game:GetService("ReplicatedStorage")
local SSS = game:GetService("ServerScriptService")
local out = {}
local function log(s)
table.insert(out, s)
end
local TARGETS = { "rx7_fc", "silv_s15", "gtr_r34", "hatch_junk", "hyper_x" }
local shells = {}
for _, root in ipairs({ RS, SSS, game:GetService("ServerStorage"), workspace }) do
for _, d in ipairs(root:GetDescendants()) do
if d:IsA("Model") and d:FindFirstChild("Wheels") and not shells[d.Name] then
for _, t in ipairs(TARGETS) do
if d.Name == t then
shells[d.Name] = d
end
end
end
end
end
-- Replicate trueAABB: min/max corner over all BaseParts, world space.
local function trueAABB(model)
local lo, hi
for _, d in ipairs(model:GetDescendants()) do
if d:IsA("BasePart") then
local p = d.Position
lo = lo and Vector3.new(math.min(lo.X, p.X), math.min(lo.Y, p.Y), math.min(lo.Z, p.Z)) or p
hi = hi and Vector3.new(math.max(hi.X, p.X), math.max(hi.Y, p.Y), math.max(hi.Z, p.Z)) or p
end
end
return lo, hi
end
log(string.format("%-11s %-10s %-22s %-14s %s", "shell", "yaw", "centreLocal(x,z)", "CURRENT err", "FIXED err"))
log(string.rep("-", 82))
for _, name in ipairs(TARGETS) do
local shell = shells[name]
if not shell then
log(string.format("%-11s NOT FOUND", name))
continue
end
-- yaw the solver would compute
local pivotInv = shell:GetPivot():Inverse()
local wf = shell:FindFirstChild("Wheels")
local fSum, fN, rSum, rN = Vector3.zero, 0, Vector3.zero, 0
for _, w in ipairs(wf: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 lp = pivotInv * (sum / n)
local h = w.Name:sub(1, 1):upper()
if h == "F" then
fSum, fN = fSum + lp, fN + 1
elseif h == "R" then
rSum, rN = rSum + lp, rN + 1
end
end
end
local yaw = 0
if fN > 0 and rN > 0 then
local nose = (fSum / fN) - (rSum / rN)
if math.abs(nose.X) + math.abs(nose.Z) > 0.05 then
yaw = -math.atan2(nose.X, -nose.Z)
end
end
local lo2, hi2 = trueAABB(shell)
local centreLocal = shell:GetPivot():Inverse() * ((lo2 + hi2) / 2)
local centreFix = CFrame.new(-centreLocal.X, 0, -centreLocal.Z)
-- Simulate both orderings against an identity body at the origin.
local body = CFrame.new()
local rot = CFrame.Angles(0, yaw, 0)
-- Where does the shell's AABB centre land, relative to the body axis?
-- Model the centre as a point at centreLocal in the shell's pivot frame.
local centrePoint = CFrame.new(centreLocal.X, 0, centreLocal.Z)
local currentCF = body * rot * centreFix
local fixedCF = body * centreFix * rot -- translation applied BEFORE rotation
local curCentre = (currentCF * centrePoint).Position
local fixCentre = (fixedCF * centrePoint).Position
local curErr = math.sqrt(curCentre.X ^ 2 + curCentre.Z ^ 2)
local fixErr = math.sqrt(fixCentre.X ^ 2 + fixCentre.Z ^ 2)
log(
string.format(
"%-11s %-10s (%+.2f,%+.2f)%-8s %-14s %s",
name,
string.format("%+.0f deg", math.deg(yaw)),
centreLocal.X,
centreLocal.Z,
"",
string.format("%.3f", curErr),
string.format("%.3f", fixErr)
)
)
end
log("")
log("err = how far the shell's AABB centre sits from the chassis axis (studs).")
log("A large CURRENT err on the 180-degree shells with a near-zero FIXED err")
log("confirms the ordering bug. If both columns match, the hypothesis is WRONG")
log("and the offset is not the cause of the reversal.")
return table.concat(out, "\n")
+161
View File
@@ -0,0 +1,161 @@
--[[
diag_contact.luau -- the front wheels now steer correctly (0.01 deg error)
but the car still will not change heading (1.4 deg over 5 s, understeer
3465x). Steering geometry is therefore no longer the problem: the tyres
are producing no lateral force.
Most likely cause: the hub is welded RIGIDLY to the body with no
suspension travel, and the Body part is CanCollide=true. If the body
is resting on the pad, the car slides on a big low-friction box and the
wheels are carrying little or no load.
This measures actual ground contact and per-wheel load.
]]
local ServerScriptService = game:GetService("ServerScriptService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
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
local Shared = ReplicatedStorage:FindFirstChild("Shared")
local Cars = require(Shared.Cars)
local Chassis = require(Shared.Chassis)
local orig = ServerScriptService:FindFirstChild("Server", true):FindFirstChild("CarManager")
local clone = orig:Clone()
clone.Parent = orig.Parent
local CarManager = require(clone)
local pad = Instance.new("Part")
pad.Size = Vector3.new(6000, 8, 6000)
pad.Position = Vector3.new(0, 5000, 0)
pad.Anchored = true
pad.Material = Enum.Material.Concrete
pad.Parent = workspace
local padTop = pad.Position.Y + 4
log(string.format("pad top surface Y = %.2f", padTop))
local car = Cars.Get("gtr_r34")
local tuning = Chassis.tuningFor(car)
local model = CarManager._buildForDiag(car, tuning)
model.Parent = workspace
model:PivotTo(CFrame.new(0, padTop + 3, 0))
local body = model.PrimaryPart
local wheels, servos, motors = {}, {}, {}
for _, d in ipairs(model:GetDescendants()) do
if d:IsA("BasePart") and d.Name:sub(1, 6) == "Wheel_" then
table.insert(wheels, d)
elseif d:IsA("HingeConstraint") then
if d.Name == Chassis.STEER_SERVO_NAME then
table.insert(servos, d)
elseif d.Name == Chassis.WHEEL_MOTOR_NAME then
table.insert(motors, d)
end
end
end
table.sort(wheels, function(a, b)
return a.Name < b.Name
end)
for _ = 1, 120 do
RunService.Heartbeat:Wait()
end
log("")
log("--- AT REST ---")
log(
string.format(
"body: centre Y %.2f, size %s, CanCollide %s",
body.Position.Y,
tostring(body.Size),
tostring(body.CanCollide)
)
)
log(
string.format(
"body underside Y = %.2f (pad top %.2f) -> gap %.2f %s",
body.Position.Y - body.Size.Y / 2,
padTop,
(body.Position.Y - body.Size.Y / 2) - padTop,
((body.Position.Y - body.Size.Y / 2) - padTop) < 0.05 and "*** BODY ON THE GROUND ***" or "(clear)"
)
)
log(string.format("WHEEL_RADIUS %.2f", Chassis.WHEEL_RADIUS))
for _, w in ipairs(wheels) do
local bottom = w.Position.Y - Chassis.WHEEL_RADIUS
log(
string.format(
" %-10s centre Y %7.2f bottom %7.2f gap to pad %6.2f %s",
w.Name,
w.Position.Y,
bottom,
bottom - padTop,
math.abs(bottom - padTop) < 0.1 and "CONTACT" or ((bottom - padTop) > 0.1 and "FLOATING" or "SUNK")
)
)
end
-- Who is actually touching the pad?
log("")
log("--- PARTS TOUCHING THE PAD ---")
local touching = pad:GetTouchingParts()
local names = {}
for _, p in ipairs(touching) do
if p:IsDescendantOf(model) then
names[#names + 1] = p.Name
end
end
log(#names > 0 and (" " .. table.concat(names, ", ")) or " <nothing from the car>")
-- Rolling, steered: is the body dragging?
log("")
log("--- ROLLING @ ~200, STEERED -14 ---")
local topW = tuning.MaxWheelAngularVelocity
for step = 1, 240 do
for _, mo in ipairs(motors) do
mo.MotorMaxTorque = tuning.MotorMaxTorque
mo.AngularVelocity = -topW
end
for _, s in ipairs(servos) do
s.TargetAngle = -14
end
if step % 60 == 0 then
local contactNames = {}
for _, p in ipairs(pad:GetTouchingParts()) do
if p:IsDescendantOf(model) then
contactNames[#contactNames + 1] = p.Name
end
end
local slip = body.AssemblyLinearVelocity
local fwd = body.CFrame.LookVector
local right = body.CFrame.RightVector
log(
string.format(
"t %.1f speed %6.1f fwd %6.1f lateral %6.1f yaw %6.3f bodyGap %5.2f touching: %s",
step / 60,
slip.Magnitude,
slip:Dot(fwd),
slip:Dot(right),
body.AssemblyAngularVelocity.Y,
(body.Position.Y - body.Size.Y / 2) - padTop,
#contactNames > 0 and table.concat(contactNames, ",") or "none"
)
)
end
RunService.Heartbeat:Wait()
end
model:Destroy()
pad:Destroy()
clone:Destroy()
return table.concat(out, "\n")
+159
View File
@@ -0,0 +1,159 @@
--!strict
-- Verify the rebuilt Daikoku PA junction IN-ENGINE, after MapBuilder has run.
--
-- MEASURE FIRST (project rule): ray tests and code reasoning both mislead;
-- actual part positions are ground truth. This reads the built parts back out
-- of Workspace rather than trusting the source that generated them.
--
-- Checks:
-- 1. No PA ring/parapet part intrudes into the mainline carriageway.
-- 2. Mainline deck clears the ring/apron by a usable height.
-- 3. Both ramps form a continuous, climbable chain (no cliff, no gap).
-- 4. The old PARingPillar parts are gone.
local ws = workspace
local function findFirst(name: string): Instance?
return ws:FindFirstChild(name, true)
end
local out = {}
local function say(s: string)
table.insert(out, s)
end
-- Locate the PA + road containers -------------------------------------------
local pa = findFirst("ParkingArea") or findFirst("PA") or findFirst("DaikokuPA")
local road = findFirst("Road")
local struct = findFirst("Structure") or findFirst("Struct")
say("== CONTAINERS ==")
say(("pa=%s road=%s struct=%s"):format(
pa and pa:GetFullName() or "NIL",
road and road:GetFullName() or "NIL",
struct and struct:GetFullName() or "NIL"
))
-- Gather parts by name pattern anywhere under Workspace ----------------------
local buckets: { [string]: { BasePart } } = {}
local function bucket(key: string, p: BasePart)
buckets[key] = buckets[key] or {}
table.insert(buckets[key], p)
end
for _, d in ipairs(ws:GetDescendants()) do
if d:IsA("BasePart") then
local n = d.Name
if n:match("^PARingWall") or n:match("^PARingRail") then
bucket("ringwall", d)
elseif n:match("^PARingPillar") then
bucket("ringpillar", d)
elseif n:match("^PARamp") and n:match("Pillar") then
bucket("ramppillar", d)
elseif n:match("^PARampKerb") then
bucket("rampkerb", d)
elseif n:match("^PAOnRamp") then
bucket("entryramp", d)
elseif n:match("^PAOffRamp") then
bucket("exitramp", d)
elseif n:match("^Road_") then
bucket("roaddeck", d)
end
end
end
say("")
say("== PART COUNTS ==")
for _, k in ipairs({ "roaddeck", "ringwall", "ringpillar", "entryramp", "exitramp", "ramppillar", "rampkerb" }) do
say(("%-12s %d"):format(k, buckets[k] and #buckets[k] or 0))
end
-- 4. Old ring pillars must be gone ------------------------------------------
say("")
say("== CHECK: legacy ring pillars removed ==")
local rp = buckets["ringpillar"]
if rp and #rp > 0 then
say(("FAIL: %d PARingPillar parts still present (ring should be ground-level)"):format(#rp))
else
say("PASS: no PARingPillar parts (ring is ground-level)")
end
-- 1 + 2. Ring structures vs mainline carriageway -----------------------------
say("")
say("== CHECK: ring walls do not intrude into the carriageway ==")
local decks = buckets["roaddeck"] or {}
local walls = buckets["ringwall"] or {}
local worstGap, worstDesc = math.huge, "n/a"
local intrusions = 0
for _, w in ipairs(walls) do
local wTop = w.Position.Y + w.Size.Y / 2
for _, dk in ipairs(decks) do
local dxz = (Vector3.new(dk.Position.X, 0, dk.Position.Z) - Vector3.new(w.Position.X, 0, w.Position.Z)).Magnitude
-- Only decks whose footprint actually overlaps this wall matter.
if dxz < (dk.Size.X / 2 + w.Size.X / 2 + 30) then
local deckBottom = dk.Position.Y - dk.Size.Y / 2
local gap = deckBottom - wTop
if gap < worstGap then
worstGap = gap
worstDesc = ("%s vs %s dxz=%.1f"):format(dk.Name, w.Name, dxz)
end
if gap < 0 then
intrusions += 1
end
end
end
end
if worstGap == math.huge then
say("PASS: no ring wall shares a footprint with any mainline deck")
else
say(("closest ring-wall/deck pair: %s"):format(worstDesc))
say(("vertical gap = %.1f studs"):format(worstGap))
if intrusions > 0 then
say(("FAIL: %d wall/deck pairs overlap vertically -- wall in the road"):format(intrusions))
else
say("PASS: every overlapping ring wall passes UNDER the deck")
end
end
-- 3. Ramp continuity ---------------------------------------------------------
local function checkRamp(key: string, label: string)
say("")
say(("== CHECK: %s continuity =="):format(label))
local segs = buckets[key]
if not segs or #segs == 0 then
say("FAIL: no segments found")
return
end
table.sort(segs, function(a, b)
local ai = tonumber(a.Name:match("_(%d+)$")) or 0
local bi = tonumber(b.Name:match("_(%d+)$")) or 0
return ai < bi
end)
local loY = segs[1].Position.Y
local hiY = segs[#segs].Position.Y
say(("%d segments, Y from %.1f to %.1f (rise %.1f)"):format(#segs, loY, hiY, hiY - loY))
local maxStep, maxGap = 0, 0
for i = 2, #segs do
local a, b = segs[i - 1], segs[i]
local step = math.abs(b.Position.Y - a.Position.Y)
local horiz = (Vector3.new(a.Position.X, 0, a.Position.Z) - Vector3.new(b.Position.X, 0, b.Position.Z)).Magnitude
-- gap = horizontal distance minus the two half-lengths that face
-- each other; negative means the segments overlap (good).
local gap = horiz - (a.Size.Z / 2 + b.Size.Z / 2)
maxStep = math.max(maxStep, step)
maxGap = math.max(maxGap, gap)
end
say(("max vertical step between segments = %.2f studs"):format(maxStep))
say(("max horizontal gap between segments = %.2f studs (negative = overlap)"):format(maxGap))
if maxStep > 6 then
say("FAIL: a step that large is a cliff a car cannot climb")
elseif maxGap > 0.5 then
say("FAIL: segments do not meet -- wheels will drop into the seam")
else
say("PASS: ramp is continuous and climbable")
end
end
checkRamp("entryramp", "entry ramp")
checkRamp("exitramp", "exit ramp")
return table.concat(out, "\n")
+72
View File
@@ -0,0 +1,72 @@
--[[
diag_editstate.luau -- read CarManager in the EDIT datamodel.
This is the datamodel that PERSISTS (unlike Play, which is a sandbox that
is discarded on stop -- the thing that silently ate every previous push).
Purpose: capture a BEFORE snapshot so that after Rojo connects we can prove
the sync actually replaced the source, rather than assuming it did. Records
length and the two markers unique to the current fixes:
SHELL_YAW_OVERRIDE -> facing fix for hatch_junk / hyper_x (NEW, x3 in src)
bestVol -> pre-existing var, NOT a freshness signal (x6 in src)
Note bestVol existed in the old code too (git HEAD line 479), so it proves
nothing on its own -- SHELL_YAW_OVERRIDE is the real discriminator.
Disk source is 41650 bytes; anything at 30631 is the old pre-fix version.
]]
local ServerScriptService = 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
log("=== EDIT datamodel: CarManager state ===")
local found = {}
for _, d in ipairs(ServerScriptService:GetDescendants()) do
if d:IsA("ModuleScript") and d.Name == "CarManager" then
found[#found + 1] = d
end
end
log("instances found: " .. #found)
if #found == 0 then
log(" !! no CarManager in ServerScriptService at all")
log(" Rojo sync will CREATE it under ServerScriptService.Server")
end
for _, m in ipairs(found) do
local src = ""
pcall(function()
src = m.Source
end)
local hasOverride = src:find("SHELL_YAW_OVERRIDE", 1, true) ~= nil
log(string.format(" %s", m:GetFullName()))
log(string.format(" len=%d (disk source = 41650, old = 30631)", #src))
log(string.format(" SHELL_YAW_OVERRIDE=%s <-- the real freshness marker", tostring(hasOverride)))
log(string.format(" verdict: %s", hasOverride and "FIXED source present" or "OLD pre-fix source"))
end
-- Also note whether the stale proof marker survived (it should NOT have --
-- it only ever existed in the discarded Play sandbox).
local marker = nil
for _, d in ipairs(workspace:GetDescendants()) do
if d.Name == "HERMES_PROOF_MARKER" then
marker = d
break
end
end
log("")
log(
"HERMES_PROOF_MARKER in Edit: "
.. (marker and "present (unexpected)" or "absent (expected - Play sandbox discarded)")
)
return table.concat(out, "\n")
+86
View File
@@ -0,0 +1,86 @@
--[[
diag_elevation.luau -- measure the vertical relationship between the wangan
mainline and the Daikoku PA, so the "elevate the highway over the PA"
rebuild can be designed from real numbers instead of guesses.
Reports, for the stretch of mainline nearest the PA:
* the deck Y profile (is it flat? already climbing? in a tunnel?)
* which SIDE of the carriageway the PA sits on, per sample
* perpendicular distance from the mainline centreline to the PA centre
* terrain/ground height under the PA and under the mainline
Read-only.
]]
local RS = game:GetService("ReplicatedStorage")
local Workspace = game:GetService("Workspace")
local Circuit = require(RS:WaitForChild("Shared"):WaitForChild("Circuit"))
local samples = Circuit.samples
local c = Circuit.PA_CENTER
local n = #samples
-- Nearest non-tunnel sample = where planPA anchors the junctions.
local bestIdx, bestD = 1, math.huge
for i, s in ipairs(samples) do
local d = (Vector3.new(s.pos.X, 0, s.pos.Z) - Vector3.new(c.X, 0, c.Z)).Magnitude
if not s.tunnel and d < bestD then
bestIdx, bestD = i, d
end
end
-- Ground probe: ray straight down, ignoring the built map so we hit terrain.
local map = Workspace:FindFirstChild("Map")
local rp = RaycastParams.new()
rp.FilterType = Enum.RaycastFilterType.Exclude
rp.FilterDescendantsInstances = map and { map } or {}
local function groundY(x, z, fromY)
local hit = Workspace:Raycast(Vector3.new(x, fromY, z), Vector3.new(0, -4000, 0), rp)
return hit and math.floor(hit.Position.Y * 10) / 10 or nil
end
local rows = {}
for k = -30, 30 do
local i = ((bestIdx + k - 1) % n) + 1
local s = samples[i]
local nxt = samples[(i % n) + 1]
local fwd = (nxt.pos - s.pos)
fwd = Vector3.new(fwd.X, 0, fwd.Z)
if fwd.Magnitude < 0.01 then
continue
end
fwd = fwd.Unit
local right = Vector3.new(-fwd.Z, 0, fwd.X)
local toPA = Vector3.new(c.X - s.pos.X, 0, c.Z - s.pos.Z)
table.insert(rows, {
k = k,
i = i,
y = math.floor(s.pos.Y * 10) / 10,
tun = s.tunnel and 1 or 0,
-- +1 => PA is to the RIGHT of travel, -1 => LEFT
side = (toPA.Unit:Dot(right) >= 0) and 1 or -1,
-- signed perpendicular offset and along-track offset to the PA centre
perp = math.floor(toPA:Dot(right)),
along = math.floor(toPA:Dot(fwd)),
dist = math.floor(toPA.Magnitude),
})
end
local ys = {}
for _, r in ipairs(rows) do
table.insert(ys, r.y)
end
table.sort(ys)
return game:GetService("HttpService"):JSONEncode({
bestIdx = bestIdx,
bestDist = math.floor(bestD),
paY = c.Y,
mainYmin = ys[1],
mainYmax = ys[#ys],
groundUnderPA = groundY(c.X, c.Z, c.Y + 500),
groundUnderMain = groundY(samples[bestIdx].pos.X, samples[bestIdx].pos.Z, samples[bestIdx].pos.Y + 500),
ringRadius = (require(RS.Shared.Circuit) and Circuit.PA_RADIUS + 130) or 0,
rows = rows,
})
+182
View File
@@ -0,0 +1,182 @@
--[[
diag_forward.luau -- settle which end of the car is the NOSE.
Contradiction to resolve: the chassis places its FRONT axle at local -Z
(WHEELBASE_HALF = 5.9, frontZ = -5.90) but Body.LookVector is +Z. Those
disagree: the part the code calls "front" is BEHIND the direction the body
looks. One of the two conventions is wrong, and the shell is being aligned
to the wrong one -- which is why the car renders backwards even though
shell and body agree (yawDelta 0).
Ground truth used here, in order of reliability:
1. VehicleSeat.LookVector -- in Roblox a seated driver faces the seat's
+Z look direction and positive throttle drives that way. This is the
direction the car ACTUALLY moves, so it defines "forward" for gameplay.
2. Steering: which wheels have a steering constraint. Only the FRONT
wheels steer, so a steered wheel's local Z tells us where the front is.
3. Shell art extents: the visual mesh bounding box relative to its pivot.
Reports each independently so a disagreement is visible rather than assumed.
]]
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
local cars = {}
for _, d in ipairs(workspace:GetDescendants()) do
if d:IsA("Model") and d.Name:match("^Car_") then
cars[#cars + 1] = d
end
end
if #cars == 0 then
return "No live Car_* models."
end
for _, model in ipairs(cars) do
log("=== " .. model.Name .. " ===")
local body = model:FindFirstChild("Body")
if not (body and body:IsA("BasePart")) then
log(" no Body part")
continue
end
local bodyCF = body.CFrame
local inv = bodyCF:Inverse()
log(
string.format(
" bodyLook (world) = (%.2f, %.2f, %.2f)",
bodyCF.LookVector.X,
bodyCF.LookVector.Y,
bodyCF.LookVector.Z
)
)
-- 1. VehicleSeat: the direction the car actually drives.
local seat = nil
for _, d in ipairs(model:GetDescendants()) do
if d:IsA("VehicleSeat") then
seat = d
break
end
end
if seat then
local seatLocalZ = (inv * seat.CFrame).Z
local dot = bodyCF.LookVector:Dot(seat.CFrame.LookVector)
log(
string.format(
" [1] VehicleSeat: localZ=%.2f, seatLook.dot(bodyLook)=%.2f -> seat faces %s body",
seatLocalZ,
dot,
(dot > 0) and "SAME as" or "OPPOSITE to"
)
)
log(string.format(" => car drives toward body %s Z", (dot > 0) and "+" or "-"))
else
log(" [1] no VehicleSeat found")
end
-- 2. Which wheels steer? Only the front axle has a steering servo.
local steered, plain = {}, {}
for _, d in ipairs(model:GetDescendants()) do
if d:IsA("BasePart") and d.Name:match("^Wheel_") then
local z = (inv * d.CFrame).Z
local hasServo = false
for _, c in ipairs(d:GetDescendants()) do
if c:IsA("HingeConstraint") and c.ActuatorType == Enum.ActuatorType.Servo then
hasServo = true
break
end
end
-- servo may live on the attachment's parent chain instead
if not hasServo and d.Parent then
for _, c in ipairs(d.Parent:GetChildren()) do
if c:IsA("HingeConstraint") and c.ActuatorType == Enum.ActuatorType.Servo then
local a1, a0 = c.Attachment1, c.Attachment0
if (a1 and a1:IsDescendantOf(d)) or (a0 and a0:IsDescendantOf(d)) then
hasServo = true
break
end
end
end
end
table.insert(hasServo and steered or plain, { name = d.Name, z = z })
end
end
local function meanZ(t)
if #t == 0 then
return 0 / 0
end
local s = 0
for _, v in ipairs(t) do
s = s + v.z
end
return s / #t
end
local sz, pz = meanZ(steered), meanZ(plain)
log(string.format(" [2] steered wheels=%d meanZ=%.2f | unsteered=%d meanZ=%.2f", #steered, sz, #plain, pz))
if #steered > 0 and #plain > 0 then
log(string.format(" => FRONT (steered) axle is at body %s Z", (sz < pz) and "-" or "+"))
end
for _, v in ipairs(steered) do
log(string.format(" steered: %-14s z=%.2f", v.name, v.z))
end
for _, v in ipairs(plain) do
log(string.format(" plain : %-14s z=%.2f", v.name, v.z))
end
-- 3. Shell art: where is the visual mass, and is it symmetric?
local shell = nil
for _, c in ipairs(model:GetChildren()) do
if c:IsA("Model") then
shell = c
break
end
end
if shell then
local cf, size = shell:GetBoundingBox()
local localCentre = (inv * cf.Position)
log(
string.format(
" [3] shell bbox size=(%.1f, %.1f, %.1f) centre localZ=%.2f",
size.X,
size.Y,
size.Z,
localCentre.Z
)
)
-- named bodywork gives an independent nose/tail cue
for _, d in ipairs(shell:GetDescendants()) do
local n = d.Name:lower()
if
d:IsA("BasePart")
and (
n:match("hood")
or n:match("bonnet")
or n:match("boot")
or n:match("trunk")
or n:match("spoiler")
or n:match("windscreen")
or n:match("bumper")
)
then
log(string.format(" %-16s localZ=%.2f", d.Name, (inv * d.CFrame).Z))
end
end
else
log(" [3] no shell model child")
end
log("")
end
return table.concat(out, "\n")
+109
View File
@@ -0,0 +1,109 @@
--[[
diag_frontjoint.luau -- is the front wheel still ATTACHED?
My RWD change returns early before creating the motor hinge, which is the
ONLY joint that ever connected Wheel_F* to Knuckle_F*. If nothing else
joins them, the front wheels are now free-floating parts that merely start
in the right place -- which would explain both observations at once:
the servo turns perfectly (it has nothing to fight) AND the car generates
no lateral force (the front tyres are not attached to the chassis).
The same doubt applies to the earlier V7 "front motors disabled" result:
Enabled=false may have freed the servo for the same wrong reason.
]]
local ServerScriptService = game:GetService("ServerScriptService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
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
local Shared = ReplicatedStorage:FindFirstChild("Shared")
local Cars = require(Shared.Cars)
local Chassis = require(Shared.Chassis)
local orig = ServerScriptService:FindFirstChild("Server", true):FindFirstChild("CarManager")
local clone = orig:Clone()
clone.Parent = orig.Parent
local CarManager = require(clone)
local pad = Instance.new("Part")
pad.Size = Vector3.new(6000, 8, 6000)
pad.Position = Vector3.new(0, 5000, 0)
pad.Anchored = true
pad.Parent = workspace
local padTop = pad.Position.Y + 4
local car = Cars.Get("gtr_r34")
local tuning = Chassis.tuningFor(car)
local model = CarManager._buildForDiag(car, tuning)
model.Parent = workspace
model:PivotTo(CFrame.new(0, padTop + 3, 0))
local function get(n)
for _, d in ipairs(model:GetDescendants()) do
if d:IsA("BasePart") and d.Name == n then
return d
end
end
end
log("Joints touching each WHEEL part:")
for _, wn in ipairs({ "Wheel_FL", "Wheel_FR", "Wheel_RL", "Wheel_RR" }) do
local w = get(wn)
local found = {}
for _, d in ipairs(model:GetDescendants()) do
local p0, p1, kind
if d:IsA("WeldConstraint") then
p0, p1, kind = d.Part0, d.Part1, "Weld"
elseif d:IsA("Constraint") then
p0 = d.Attachment0 and d.Attachment0.Parent
p1 = d.Attachment1 and d.Attachment1.Parent
kind = d.ClassName .. "/" .. d.Name
end
if p0 and p1 and (p0 == w or p1 == w) then
found[#found + 1] = string.format("%s(%s<->%s)", kind, p0.Name, p1.Name)
end
end
log(string.format(" %-10s %s", wn, #found > 0 and table.concat(found, " ") or "*** NO JOINTS -- DETACHED ***"))
end
-- Drop test: a detached wheel will not follow the body when the body is moved.
log("")
log("TELEPORT TEST (move body 300 studs; attached wheels follow):")
local body = model.PrimaryPart
for _ = 1, 60 do
RunService.Heartbeat:Wait()
end
local before = {}
for _, wn in ipairs({ "Wheel_FL", "Wheel_RL" }) do
before[wn] = get(wn).Position
end
model:PivotTo(model:GetPivot() * CFrame.new(0, 0, -300))
for _ = 1, 60 do
RunService.Heartbeat:Wait()
end
for _, wn in ipairs({ "Wheel_FL", "Wheel_RL" }) do
local w = get(wn)
local moved = (w.Position - before[wn]).Magnitude
log(
string.format(
" %-10s moved %7.1f studs %s",
wn,
moved,
moved > 100 and "followed the car" or "*** LEFT BEHIND (detached) ***"
)
)
end
model:Destroy()
pad:Destroy()
clone:Destroy()
return table.concat(out, "\n")
+67
View File
@@ -0,0 +1,67 @@
--[[
diag_line277.luau -- the build harness died at CarManager:277 with
"attempt to concatenate string with nil", but line 277 of the LOCAL file is
a comment. So the code that ran is not the code on disk.
Two candidates:
(a) the pushed Source is out of sync with disk
(b) require() handed back a STALE CACHED module from earlier this session
(the known Studio trap: editing Source does NOT invalidate the cache)
This reads the Source text directly -- never require() -- and prints the
real lines around 277, plus the reverted-convention markers, so we can tell
(a) from (b) instead of guessing.
]]
local RS = game:GetService("ReplicatedStorage")
local SSS = game:GetService("ServerScriptService")
local function find(name)
for _, root in ipairs({ SSS, RS, game:GetService("ServerStorage"), workspace }) do
local m = root:FindFirstChild(name, true)
if m and m:IsA("ModuleScript") then
return m
end
end
return nil
end
local cm = find("CarManager")
if not cm then
return "FATAL: CarManager not found"
end
local src = cm.Source
local lines = {}
for l in (src .. "\n"):gmatch("([^\n]*)\n") do
table.insert(lines, l)
end
local out = {}
table.insert(out, string.format("%s %d bytes, %d lines", cm:GetFullName(), #src, #lines))
table.insert(out, "")
table.insert(out, "--- Studio Source lines 270..284 ---")
for i = 270, math.min(284, #lines) do
table.insert(out, string.format("%4d | %s", i, lines[i]))
end
table.insert(out, "")
table.insert(out, "--- markers ---")
table.insert(out, " isFront = zSign < 0 present : " .. tostring(src:find("isFront = zSign < 0", 1, true) ~= nil))
table.insert(out, " _buildForDiag present : " .. tostring(src:find("_buildForDiag", 1, true) ~= nil))
-- Where does _buildForDiag get assigned, per the Studio copy?
for i, l in ipairs(lines) do
if l:find("_buildForDiag", 1, true) then
table.insert(out, string.format(" _buildForDiag at line %d: %s", i, l))
end
end
-- What does buildCarModel expect? Print its declaration line.
for i, l in ipairs(lines) do
if l:find("function buildCarModel", 1, true) or l:find("local function buildCarModel", 1, true) then
table.insert(out, string.format(" buildCarModel declared line %d: %s", i, l))
end
end
return table.concat(out, "\n")
+116
View File
@@ -0,0 +1,116 @@
--[[
diag_nosecue.luau -- confirm WHICH cue is lying.
diag_shellgeom (bodywork asymmetry, wheel-independent) says the built
fleet disagrees on facing: rx7_fc +0.550, silv_s15 +0.206, hatch_junk
+0.614 all land one way, while hyper_x -0.290 and gtr_r34 -0.362 land the
other. Exactly two cars wrong -- matching the user's report.
The aligner derives facing from each shell's Wheels folder. If a shell's
F*/R* wheel labels are swapped relative to its bodywork, the aligner
confidently rotates it backwards.
This cross-checks the wheel cue against two INDEPENDENT cues that cannot
both be wrong:
* named bodywork: Hood/Windshield sit at the nose, Boot/Spoiler at the tail
* bodywork asymmetry (as before)
If wheels disagree with the bodywork, the wheel labels are the bug.
]]
local ReplicatedStorage = game:GetService("ReplicatedStorage")
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
log(string.format("%-14s %10s %10s %10s %10s %s", "car", "wheelZ", "hoodZ", "bootZ", "asym", "wheels-vs-body"))
log(string.rep("-", 78))
for _, t in ipairs(ReplicatedStorage.CarModels:GetChildren()) do
local pivotInv = t:GetPivot():Inverse()
-- cue 1: wheels
local wheelDz = nil
local wf = t:FindFirstChild("Wheels")
if wf then
local fS, fN, rS, rN = 0, 0, 0, 0
for _, w in ipairs(wf:GetChildren()) do
local p = w:IsA("BasePart") and w or w:FindFirstChildWhichIsA("BasePart", true)
if p then
local z = (pivotInv * p.Position).Z
local h = w.Name:sub(1, 1):upper()
if h == "F" then
fS, fN = fS + z, fN + 1
elseif h == "R" then
rS, rN = rS + z, rN + 1
end
end
end
if fN > 0 and rN > 0 then
wheelDz = fS / fN - rS / rN
end
end
-- cue 2: named bodywork
local function avgZ(patterns)
local s, n = 0, 0
for _, p in ipairs(t:GetDescendants()) do
if p:IsA("BasePart") then
local nm = p.Name:lower()
for _, pat in ipairs(patterns) do
if nm == pat then
s += (pivotInv * p.Position).Z
n += 1
break
end
end
end
end
return n > 0 and (s / n) or nil
end
local hoodZ = avgZ({ "hood", "windshield", "bonnet", "grille" })
local bootZ = avgZ({ "boot", "spoiler", "trunk" })
-- cue 3: asymmetry
local sum, wsum, lo, hi = 0, 0, math.huge, -math.huge
for _, p in ipairs(t:GetDescendants()) do
if p:IsA("BasePart") then
local nm = p.Name:lower()
if not (nm:find("wheel") or nm:find("tyre") or nm:find("tire") or nm:find("rim")) then
local z = (pivotInv * p.Position).Z
local v = p.Size.X * p.Size.Y * p.Size.Z
sum, wsum = sum + z * v, wsum + v
lo, hi = math.min(lo, z), math.max(hi, z)
end
end
end
local asym = wsum > 0 and (sum / wsum - (lo + hi) / 2) or nil
-- Wheels say nose is at sign(wheelDz). Bodywork says nose is where the
-- hood is, i.e. sign(hoodZ). They must agree.
local verdict = "?"
if wheelDz and hoodZ then
local wheelNose = wheelDz < 0 and -1 or 1
local hoodNose = hoodZ < 0 and -1 or 1
verdict = (wheelNose == hoodNose) and "agree" or "*** DISAGREE -- wheel labels swapped ***"
end
log(
string.format(
"%-14s %10s %10s %10s %10s %s",
t.Name,
wheelDz and string.format("%+.2f", wheelDz) or "n/a",
hoodZ and string.format("%+.2f", hoodZ) or "n/a",
bootZ and string.format("%+.2f", bootZ) or "n/a",
asym and string.format("%+.2f", asym) or "n/a",
verdict
)
)
end
return table.concat(out, "\n")
+200
View File
@@ -0,0 +1,200 @@
--[[
diag_nosecue2.luau -- settle which is lying on hatch_junk / hyper_x: the
wheel LABELS, or the shell MESH itself.
Background: diag_shelllabels.luau showed those two shells label their F
axle toward +Z while rx7_fc / silv_s15 / gtr_r34 label F toward -Z. That
matches the reported symptom exactly, but it does NOT say which side is
wrong -- either the labels are swapped, or the whole mesh is authored
facing the other way and the labels are honest.
So: gather cues that are INDEPENDENT of the wheel names, from the shell's
own parts. Every cue is read off part names/positions in the shell's local
frame; none is derived from a constant, and each can come out either way.
headlights -- sit at the NOSE
taillights -- sit at the TAIL
*front* / *rear* named parts (bumpers, grille) -- self-describing
seat -- a VehicleSeat/Seat inside the shell looks FORWARD
Interpretation:
* cues agree with labels -> the MESH is rotated; labels are honest
* cues oppose the labels -> the LABELS are swapped; mesh is fine
A shell where cues themselves disagree is reported as INCONCLUSIVE rather
than being forced into a verdict.
]]
local RS = game:GetService("ReplicatedStorage")
local SSS = game:GetService("ServerScriptService")
local SS = game:GetService("ServerStorage")
local TARGETS = { rx7_fc = true, hatch_junk = true, silv_s15 = true, gtr_r34 = true, hyper_x = true }
local out = {}
local function log(s)
table.insert(out, s)
end
-- Collect the five target shells wherever they live.
local shells = {}
for _, root in ipairs({ RS, SSS, SS, workspace }) do
for _, d in ipairs(root:GetDescendants()) do
if d:IsA("Model") and TARGETS[d.Name] and d:FindFirstChild("Wheels") and not shells[d.Name] then
shells[d.Name] = d
end
end
end
-- Mean local Z of every part whose lowercased name matches a pattern.
local function meanZ(shell, pivotInv, patterns)
local sum, n = 0, 0
for _, d in ipairs(shell:GetDescendants()) do
if d:IsA("BasePart") then
local nm = d.Name:lower()
for _, pat in ipairs(patterns) do
if nm:find(pat, 1, true) then
sum += (pivotInv * d.Position).Z
n += 1
break
end
end
end
end
if n == 0 then
return nil, 0
end
return sum / n, n
end
local order = { "rx7_fc", "silv_s15", "gtr_r34", "hatch_junk", "hyper_x" }
log(
string.format(
"%-11s %-14s %-14s %-14s %-14s %s",
"shell",
"labelF.Z",
"headlight.Z",
"taillight.Z",
"seatFwd.Z",
"verdict"
)
)
log(string.rep("-", 104))
local seen = 0
for _, name in ipairs(order) do
local shell = shells[name]
if not shell then
log(string.format("%-11s NOT FOUND", name))
continue
end
seen += 1
local pivotInv = shell:GetPivot():Inverse()
-- Wheel-label cue: sign of (F centroid - R centroid).Z
local wheels = shell:FindFirstChild("Wheels")
local fSum, fN, rSum, rN = 0, 0, 0, 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 z = (pivotInv * (sum / n)).Z
local head = w.Name:sub(1, 1):upper()
if head == "F" then
fSum, fN = fSum + z, fN + 1
elseif head == "R" then
rSum, rN = rSum + z, rN + 1
end
end
end
local labelZ = (fN > 0 and rN > 0) and ((fSum / fN) - (rSum / rN)) or nil
local headZ, headN = meanZ(shell, pivotInv, { "headlight", "headlamp", "grille", "grill" })
local tailZ, tailN = meanZ(shell, pivotInv, { "taillight", "taillamp", "tailight", "brakelight" })
-- Seat cue: a seat's LookVector, expressed in the SHELL's frame.
local seatZ = nil
for _, d in ipairs(shell:GetDescendants()) do
if d:IsA("VehicleSeat") or d:IsA("Seat") then
-- Rotate (not translate) the seat's world look into shell-local axes.
seatZ = (shell:GetPivot():Inverse() * (d.CFrame.LookVector + shell:GetPivot().Position)).Z
break
end
end
-- Each cue votes for which local Z direction is the NOSE.
-- Convention target: nose should be at -Z.
local votes = {}
if headZ then
votes[#votes + 1] = (headZ < 0) and -1 or 1
end
if tailZ then
votes[#votes + 1] = (tailZ < 0) and 1 or -1
end -- tail opposes nose
if seatZ then
votes[#votes + 1] = (seatZ < 0) and -1 or 1
end
local noseSign = nil
if #votes > 0 then
local s = 0
for _, v in ipairs(votes) do
s += v
end
if s > 0 then
noseSign = 1
elseif s < 0 then
noseSign = -1
end
end
local verdict
if not labelZ then
verdict = "no F/R labels"
elseif #votes == 0 then
verdict = "NO INDEPENDENT CUE (no lights/seat found)"
elseif noseSign == nil then
verdict = "INCONCLUSIVE (cues split)"
else
local labelSign = (labelZ < 0) and -1 or 1
if labelSign == noseSign then
verdict = (noseSign < 0) and "consistent, nose -Z (fine)" or "LABELS HONEST -> MESH is rotated 180"
else
verdict = "LABELS SWAPPED -> mesh is fine"
end
end
local function fmt(v, n)
return v and string.format("%+.1f(%d)", v, n or 0) or "--"
end
log(
string.format(
"%-11s %-14s %-14s %-14s %-14s %s",
name,
labelZ and string.format("%+.1f", labelZ) or "--",
fmt(headZ, headN),
fmt(tailZ, tailN),
seatZ and string.format("%+.2f", seatZ) or "--",
verdict
)
)
end
log("")
if seen == 0 then
log(">>> NOTHING INSPECTED -- this is a FAILURE, not a pass.")
else
log(string.format(">>> inspected %d/5 shells", seen))
log(" labelF.Z = (F centroid - R centroid).Z; negative means labels put F toward -Z")
log(" headlight.Z = mean local Z of headlight/grille parts (count); nose should be -Z")
log(" seatFwd.Z = seat LookVector Z in shell frame; negative = seat faces -Z")
end
return table.concat(out, "\n")
+155
View File
@@ -0,0 +1,155 @@
--[[
diag_paexit.luau -- work out the real route from the SpawnPad out of the
Daikoku PA and onto the wangan mainline.
Reports, in world coordinates: the spawn pad, the ring road, both ramps,
and -- crucially -- the COMPASS BEARING and distance from the pad to each
ramp, so the answer can be phrased as "drive X studs toward <direction>"
rather than a vague description.
Bearings are given in Roblox world terms: -Z is treated as North.
]]
local Workspace = game:GetService("Workspace")
local out = {}
local function log(s)
table.insert(out, s)
end
local map = Workspace:FindFirstChild("Map")
if not map then
return "FATAL: no Map in Workspace -- build the map first (Play once)"
end
local function grab(name)
local hit = map:FindFirstChild(name, true)
return (hit and hit:IsA("BasePart")) and hit or nil
end
local pad = grab("SpawnPad")
if not pad then
return "FATAL: SpawnPad not found under Map"
end
log(
string.format(
"SpawnPad pos=(%.0f, %.0f, %.0f) size=(%.0f x %.0f)",
pad.Position.X,
pad.Position.Y,
pad.Position.Z,
pad.Size.X,
pad.Size.Z
)
)
-- Compass bearing from the pad to a target, with -Z = North.
local function bearing(from, to)
local d = Vector3.new(to.X - from.X, 0, to.Z - from.Z)
if d.Magnitude < 0.01 then
return "--", 0
end
local deg = math.deg(math.atan2(d.X, -d.Z)) -- 0 = -Z (north), +90 = +X (east)
if deg < 0 then
deg += 360
end
local names = { "North", "NE", "East", "SE", "South", "SW", "West", "NW" }
local idx = math.floor(((deg + 22.5) % 360) / 45) + 1
return names[idx], d.Magnitude, deg
end
log("")
log("--- ramps and ring ---")
for _, nm in ipairs({ "PAOnRamp", "PAOffRamp" }) do
local p = grab(nm)
if p then
local dir, dist, deg = bearing(pad.Position, p.Position)
log(
string.format(
"%-10s pos=(%.0f, %.0f, %.0f) %s of pad, %.0f studs, bearing %.0f deg",
nm,
p.Position.X,
p.Position.Y,
p.Position.Z,
dir,
dist,
deg or 0
)
)
else
log(string.format("%-10s NOT FOUND", nm))
end
end
-- Ring segments: report the ring's radius and height vs the pad.
local ringParts = {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") and (d.Name:find("Ring") or d.Name == "PARing") then
table.insert(ringParts, d)
end
end
if #ringParts > 0 then
local minY, maxY = math.huge, -math.huge
local maxR = 0
for _, p in ipairs(ringParts) do
minY = math.min(minY, p.Position.Y)
maxY = math.max(maxY, p.Position.Y)
local r = (Vector3.new(p.Position.X - pad.Position.X, 0, p.Position.Z - pad.Position.Z)).Magnitude
maxR = math.max(maxR, r)
end
log(
string.format(
"ring %d parts, radius up to %.0f studs, Y %.0f..%.0f (pad Y %.0f)",
#ringParts,
maxR,
minY,
maxY,
pad.Position.Y
)
)
log(string.format(" ring sits %.0f studs ABOVE the pad", minY - pad.Position.Y))
end
-- Where is the mainline relative to the pad?
log("")
log("--- mainline ---")
local roadFolder = map:FindFirstChild("Road") or map:FindFirstChild("Circuit")
if roadFolder then
local nearest, nearestD = nil, math.huge
for _, d in ipairs(roadFolder:GetDescendants()) do
if d:IsA("BasePart") then
local dist = (Vector3.new(d.Position.X - pad.Position.X, 0, d.Position.Z - pad.Position.Z)).Magnitude
if dist < nearestD then
nearestD, nearest = dist, d
end
end
end
if nearest then
local dir, dist = bearing(pad.Position, nearest.Position)
log(
string.format(
"nearest mainline part '%s' at (%.0f, %.0f, %.0f)",
nearest.Name,
nearest.Position.X,
nearest.Position.Y,
nearest.Position.Z
)
)
log(
string.format(
" -> %s of the pad, %.0f studs away, Y %+.0f vs pad",
dir,
dist,
nearest.Position.Y - pad.Position.Y
)
)
end
else
local names = {}
for _, d in ipairs(map:GetChildren()) do
table.insert(names, d.Name)
end
log("no Road/Circuit folder; Map children: " .. table.concat(names, ", "))
end
return table.concat(out, "\n")
+116
View File
@@ -0,0 +1,116 @@
--[[
diag_paexit2.luau -- follow-up: the first pass looked for a "Road" folder,
but the map's mainline lives under "Expressway". Also resolve which of the
two ramps is the way OUT (the off-ramp rejoins the mainline), and give the
pad-relative direction in terms the player can act on.
]]
local Workspace = game:GetService("Workspace")
local out = {}
local function log(s)
table.insert(out, s)
end
local map = Workspace:FindFirstChild("Map")
if not map then
return "FATAL: no Map"
end
local pad = map:FindFirstChild("SpawnPad", true)
if not pad then
return "FATAL: no SpawnPad"
end
local function bearing(from, to)
local d = Vector3.new(to.X - from.X, 0, to.Z - from.Z)
if d.Magnitude < 0.01 then
return "--", 0, 0
end
local deg = math.deg(math.atan2(d.X, -d.Z))
if deg < 0 then
deg += 360
end
local names = { "North", "North-East", "East", "South-East", "South", "South-West", "West", "North-West" }
return names[math.floor(((deg + 22.5) % 360) / 45) + 1], d.Magnitude, deg
end
local expw = map:FindFirstChild("Expressway")
log(string.format("pad at (%.0f, %.0f, %.0f)", pad.Position.X, pad.Position.Y, pad.Position.Z))
log("")
if expw then
local nearest, nearestD
local count = 0
for _, d in ipairs(expw:GetDescendants()) do
if d:IsA("BasePart") then
count += 1
local dist = (Vector3.new(d.Position.X - pad.Position.X, 0, d.Position.Z - pad.Position.Z)).Magnitude
if not nearestD or dist < nearestD then
nearestD, nearest = dist, d
end
end
end
log(string.format("Expressway: %d parts", count))
if nearest then
local dir, dist = bearing(pad.Position, nearest.Position)
log(
string.format(
" nearest mainline part '%s' (%.0f, %.0f, %.0f)",
nearest.Name,
nearest.Position.X,
nearest.Position.Y,
nearest.Position.Z
)
)
log(string.format(" -> %s of pad, %.0f studs, Y %+.0f vs pad", dir, dist, nearest.Position.Y - pad.Position.Y))
end
end
-- Ramp detail: where each ramp STARTS (ring end) and ENDS (mainline end).
log("")
log("--- ramp geometry (which way out) ---")
for _, nm in ipairs({ "PAOnRamp", "PAOffRamp" }) do
local p = map:FindFirstChild(nm, true)
if not p then
log(nm .. ": NOT FOUND")
continue
end
-- A ramp deck is built with lookAt(mid, road): its LookVector points at the
-- mainline end. Report both ends so the direction of travel is unambiguous.
local halfLen = p.Size.Z / 2
local toRoad = p.CFrame * CFrame.new(0, 0, -halfLen)
local toRing = p.CFrame * CFrame.new(0, 0, halfLen)
local dir, dist = bearing(pad.Position, p.Position)
log(string.format("%s centre %s of pad, %.0f studs", nm, dir, dist))
log(string.format(" ring end (%.0f, %.0f, %.0f)", toRing.Position.X, toRing.Position.Y, toRing.Position.Z))
log(string.format(" mainline end (%.0f, %.0f, %.0f)", toRoad.Position.X, toRoad.Position.Y, toRoad.Position.Z))
log(string.format(" length %.0f studs, width %.0f", p.Size.Z, p.Size.X))
end
-- How far from the pad edge to the ring, in each cardinal direction?
log("")
log("--- getting from pad to ring ---")
log(string.format("pad is %.0f x %.0f, so its edge is %.0f studs from centre", pad.Size.X, pad.Size.Z, pad.Size.X / 2))
local ringParts = {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") and d.Name:find("Ring") then
table.insert(ringParts, d)
end
end
if #ringParts > 0 then
local minR, maxR = math.huge, 0
for _, p in ipairs(ringParts) do
local r = (Vector3.new(p.Position.X - pad.Position.X, 0, p.Position.Z - pad.Position.Z)).Magnitude
minR = math.min(minR, r)
maxR = math.max(maxR, r)
end
log(
string.format(
"ring road lies %.0f..%.0f studs from the pad centre, in every direction (it encircles the lot)",
minR,
maxR
)
)
end
return table.concat(out, "\n")
+101
View File
@@ -0,0 +1,101 @@
--[[
diag_pawall.luau -- is the parking area actually sealed?
The user reports a continuous low wall around the lot with no way out. The
suspect: planPA builds PARingWall on BOTH edges of every ring segment
(for sgn in {1,-1}) with no opening carved where PAOnRamp / PAOffRamp meet
the ring -- so the INNER lip would form an unbroken fence around the apron.
This does not reason about the code. It fires RAYS outward from the spawn
pad in 72 directions and reports what each one hits first, so a genuine gap
shows up as a ray that reaches the ring deck (or beyond) without hitting a
wall. If every ray hits PARingWall, the lot is sealed -- confirmed by
measurement, not inspection.
]]
local Workspace = game:GetService("Workspace")
local map = Workspace:FindFirstChild("Map")
if not map then
return "FATAL: no Map"
end
local pad = map:FindFirstChild("SpawnPad", true)
if not pad then
return "FATAL: no SpawnPad"
end
local origin = pad.Position + Vector3.new(0, 2, 0)
local params = RaycastParams.new()
params.FilterType = Enum.RaycastFilterType.Include
params.FilterDescendantsInstances = { map }
local out = {}
local function log(s)
table.insert(out, s)
end
local N = 72
local hits = {}
local escapes = {}
local wallH = {}
for i = 0, N - 1 do
local ang = (i / N) * math.pi * 2
local dir = Vector3.new(math.cos(ang), 0, math.sin(ang))
local res = Workspace:Raycast(origin, dir * 600, params)
local bearingDeg = math.deg(math.atan2(dir.X, -dir.Z))
if bearingDeg < 0 then
bearingDeg += 360
end
if res then
local nm = res.Instance.Name
hits[nm] = (hits[nm] or 0) + 1
if nm:find("Wall") or nm:find("Barrier") or nm:find("Rail") then
-- how tall is the blocker, and how high above the apron does it reach?
local top = res.Instance.Position.Y + res.Instance.Size.Y / 2
table.insert(wallH, top - pad.Position.Y)
else
table.insert(escapes, string.format("bearing %3.0f deg -> %s at %.0f studs", bearingDeg, nm, res.Distance))
end
else
table.insert(escapes, string.format("bearing %3.0f deg -> CLEAR (no hit in 600 studs)", bearingDeg))
end
end
log(string.format("cast %d rays outward from the pad at Y+2", N))
log("")
log("--- what the rays hit first ---")
for nm, n in pairs(hits) do
log(string.format(" %-16s %d rays", nm, n))
end
log("")
if #escapes == 0 then
log(">>> SEALED: every one of the " .. N .. " rays hit a wall/barrier.")
log(" There is NO gap in the ring wall -- the lot is fully enclosed.")
else
log(string.format(">>> %d ray(s) found a non-wall path out:", #escapes))
for _, e in ipairs(escapes) do
log(" " .. e)
end
end
if #wallH > 0 then
local mn, mx = math.huge, -math.huge
for _, h in ipairs(wallH) do
mn = math.min(mn, h)
mx = math.max(mx, h)
end
log("")
log(string.format("blocking wall top sits %.1f..%.1f studs above the apron", mn, mx))
log("(a car can climb roughly 2-3 studs; anything above that is a hard barrier)")
end
-- Distance from pad centre to the first wall, to size the enclosure.
local r = Workspace:Raycast(origin, Vector3.new(1, 0, 0) * 600, params)
if r then
log(string.format("first blocker due East is %.0f studs from pad centre", r.Distance))
end
return table.concat(out, "\n")
+120
View File
@@ -0,0 +1,120 @@
--[[
diag_seatdir.luau -- decide, empirically, which way the car DRIVES.
The contradiction: CarManager:158 says `isFront = zSign < 0 -- car forward
is -Z`, so the front axle is built at local -Z. But the VehicleSeat's
LookVector points +Z (measured: seatLook.dot(bodyLook) = 1.00).
In Roblox a VehicleSeat applies throttle along its OWN +Z LookVector, so
positive throttle drives the car toward +Z -- i.e. toward the axle the code
labels REAR. If that holds, every car drives "backwards" and the shell,
which is aligned to the chassis convention, renders nose-first into the
direction of travel... backwards on screen.
This does not guess: it reads the seat's actual Throttle/steer wiring and
compares the seat basis against the wheel labels, then reports which axle
leads under positive throttle.
It also checks whether the steering servo lives on the axle that leads,
because a car steering with its TRAILING axle is the classic shopping-cart
castering symptom the user reported.
]]
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
for _, model in ipairs(workspace:GetDescendants()) do
if not (model:IsA("Model") and model.Name:match("^Car_")) then
continue
end
local body = model:FindFirstChild("Body")
local seat = nil
for _, d in ipairs(model:GetDescendants()) do
if d:IsA("VehicleSeat") then
seat = d
break
end
end
if not (body and body:IsA("BasePart") and seat) then
continue
end
log("=== " .. model.Name .. " ===")
local inv = body.CFrame:Inverse()
-- Seat look expressed in BODY-LOCAL coords: which local axis does the car
-- drive along under positive throttle?
local seatLookLocal = inv:VectorToObjectSpace(seat.CFrame.LookVector)
log(
string.format(
" seat look in body-local = (%.2f, %.2f, %.2f)",
seatLookLocal.X,
seatLookLocal.Y,
seatLookLocal.Z
)
)
local drivesTowardPosZ = seatLookLocal.Z > 0
log(string.format(" => positive throttle drives toward local %sZ", drivesTowardPosZ and "+" or "-"))
-- Where are the labelled axles, and which one has the steering servo?
local fZ, fN, rZ, rN = 0, 0, 0, 0
local steerAxleZ, steerCount = 0, 0
for _, d in ipairs(model:GetDescendants()) do
if d:IsA("BasePart") and d.Name:match("^Wheel_") then
local z = (inv * d.CFrame).Z
if d.Name:sub(7, 7):upper() == "F" then
fZ, fN = fZ + z, fN + 1
else
rZ, rN = rZ + z, rN + 1
end
end
-- servo is parented to the HUB, not the wheel -- find it by name
if d:IsA("HingeConstraint") and d.ActuatorType == Enum.ActuatorType.Servo then
local host = d.Parent
if host and host:IsA("BasePart") then
steerAxleZ = steerAxleZ + (inv * host.CFrame).Z
steerCount = steerCount + 1
end
end
end
if fN > 0 and rN > 0 then
local fm, rm = fZ / fN, rZ / rN
log(string.format(" labelled FRONT axle localZ=%.2f | REAR axle localZ=%.2f", fm, rm))
local leadingIsFront = (drivesTowardPosZ and fm > rm) or (not drivesTowardPosZ and fm < rm)
log(
string.format(
" => the axle LEADING under throttle is the one labelled %s",
leadingIsFront and "FRONT (correct)" or "REAR (car drives backwards!)"
)
)
end
if steerCount > 0 then
local sm = steerAxleZ / steerCount
log(string.format(" steering servos=%d, mean host localZ=%.2f", steerCount, sm))
local steerLeads = (drivesTowardPosZ and sm > 0) or (not drivesTowardPosZ and sm < 0)
log(
string.format(
" => steered axle %s under throttle %s",
steerLeads and "LEADS" or "TRAILS",
steerLeads and "(normal car)" or "(SHOPPING-CART castering)"
)
)
else
log(" no steering servos found by name scan")
end
log("")
end
if #out == 0 then
return "No live cars with a Body + VehicleSeat."
end
return table.concat(out, "\n")
+109
View File
@@ -0,0 +1,109 @@
--[[
diag_shellbuilt.luau -- does the solved yaw actually reach the built car?
diag_shellyaw2 proved the aligner SEES the problem: hatch_junk and hyper_x
are authored nose-toward +Z and solve to yaw ~180, the other three to ~0.
That is exactly the "at least two cars are backwards" the user reports, so
the cue is right and the question is whether the rotation survives to the
built model.
The earlier attempt measured -11.80 for every car -- that is the CHASSIS
wheelbase, i.e. it matched the chassis Wheel_* parts, not the shell's own
Wheels folder. This one scopes the search strictly INSIDE the Shell model
and skips anything named Wheel_* (the chassis convention), so it can only
ever read shell wheels.
]]
local ServerScriptService = game:GetService("ServerScriptService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
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
local Shared = ReplicatedStorage:FindFirstChild("Shared")
local Cars = require(Shared.Cars)
local Chassis = require(Shared.Chassis)
local orig = ServerScriptService:FindFirstChild("Server", true):FindFirstChild("CarManager")
local clone = orig:Clone()
clone.Parent = orig.Parent
local CarManager = require(clone)
local pad = Instance.new("Part")
pad.Size = Vector3.new(400, 8, 400)
pad.Position = Vector3.new(0, 5000, 0)
pad.Anchored = true
pad.Parent = workspace
log("chassis forward = -Z. Shell nose must land at NEGATIVE body-local Z.")
log("")
for _, template in ipairs(ReplicatedStorage.CarModels:GetChildren()) do
local id = template.Name
local car = Cars.Get(id)
if not car then
log(string.format("%-14s <no car def for this shell id>", id))
else
local model = CarManager._buildForDiag(car, Chassis.tuningFor(car))
model.Parent = workspace
model:PivotTo(CFrame.new(0, 5010, 0))
for _ = 1, 5 do
RunService.Heartbeat:Wait()
end
local body = model.PrimaryPart
local shell = model:FindFirstChild("Shell")
if not shell then
log(string.format("%-14s <no Shell child -- using procedural CarBody>", id))
else
local inv = body.CFrame:Inverse()
-- Only look at wheels INSIDE the shell, and never at chassis Wheel_*.
local wf = shell:FindFirstChild("Wheels")
local fSum, fN, rSum, rN = Vector3.zero, 0, Vector3.zero, 0
if wf then
for _, w in ipairs(wf:GetChildren()) do
if w.Name:sub(1, 6) ~= "Wheel_" then
local part = w:IsA("BasePart") and w or w:FindFirstChildWhichIsA("BasePart", true)
if part then
local h = w.Name:sub(1, 1):upper()
local lp = inv * part.Position
if h == "F" then
fSum, fN = fSum + lp, fN + 1
elseif h == "R" then
rSum, rN = rSum + lp, rN + 1
end
end
end
end
end
if fN > 0 and rN > 0 then
local f, r = fSum / fN, rSum / rN
local dz = f.Z - r.Z
log(
string.format(
"%-14s shell wheels: frontZ %+6.2f rearZ %+6.2f dZ %+6.2f -> %s",
id,
f.Z,
r.Z,
dz,
dz < 0 and "CORRECT" or "*** STILL BACKWARDS ***"
)
)
else
log(string.format("%-14s <shell has no usable F/R wheels after build (F=%d R=%d)>", id, fN, rN))
end
end
model:Destroy()
end
end
pad:Destroy()
clone:Destroy()
return table.concat(out, "\n")
+136
View File
@@ -0,0 +1,136 @@
--[[
diag_shellgeom.luau -- measure the SHELL BODYWORK directly.
Two prior attempts both returned dZ = -11.80 for every car, which is the
chassis wheelbase: the "shell wheels" being matched are in fact the
chassis Wheel_* parts, so those runs proved nothing about shell facing.
Stop relying on wheel names. Instead:
1. Dump the real child structure of one shell so we can see what is
actually in there after the build.
2. Measure asymmetric BODYWORK mass: a car's cabin/rear sits behind its
nose, so the shell's own parts are not symmetric front-to-back.
Compare the shell part centroid against the shell AABB centre along
body-local Z -- the sign flips when the shell is rotated 180.
3. Cross-check against the same measurement on the raw TEMPLATE, where
diag_shellyaw2 already established ground truth (hatch_junk and
hyper_x authored nose-toward +Z, the other three nose-toward -Z).
If the built car's sign matches the template's, the flip never happened.
]]
local ServerScriptService = game:GetService("ServerScriptService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
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
local Shared = ReplicatedStorage:FindFirstChild("Shared")
local Cars = require(Shared.Cars)
local Chassis = require(Shared.Chassis)
local orig = ServerScriptService:FindFirstChild("Server", true):FindFirstChild("CarManager")
local cloneMod = orig:Clone()
cloneMod.Parent = orig.Parent
local CarManager = require(cloneMod)
-- 1. What is actually inside a built shell?
local car0 = Cars.Get("hyper_x")
local m0 = CarManager._buildForDiag(car0, Chassis.tuningFor(car0))
m0.Parent = workspace
m0:PivotTo(CFrame.new(0, 5010, 0))
local shell0 = m0:FindFirstChild("Shell")
log("=== STRUCTURE of built hyper_x Shell ===")
if shell0 then
for _, c in ipairs(shell0:GetChildren()) do
local extra = ""
if c:IsA("Folder") or c:IsA("Model") then
local kids = {}
for _, k in ipairs(c:GetChildren()) do
kids[#kids + 1] = k.Name
end
extra = " {" .. table.concat(kids, ",") .. "}"
end
log(string.format(" %-16s %s%s", c.Name, c.ClassName, extra))
end
else
log(" <no Shell>")
end
m0:Destroy()
log("")
-- 2/3. Bodywork asymmetry, template vs built.
local function asymmetry(root: Instance, frame: CFrame): number?
-- Volume-weighted centroid of bodywork, minus AABB centre, along local Z.
local inv = frame:Inverse()
local sum, wsum = 0, 0
local lo, hi = math.huge, -math.huge
for _, p in ipairs(root:GetDescendants()) do
if p:IsA("BasePart") then
local n = p.Name:lower()
-- exclude wheels/tyres of any naming convention
if not (n:find("wheel") or n:find("tyre") or n:find("tire") or n:find("rim")) then
local z = (inv * p.Position).Z
local vol = p.Size.X * p.Size.Y * p.Size.Z
sum += z * vol
wsum += vol
lo = math.min(lo, z)
hi = math.max(hi, z)
end
end
end
if wsum <= 0 then
return nil
end
local centroid = sum / wsum
local aabbCentre = (lo + hi) / 2
return centroid - aabbCentre
end
log("=== BODYWORK ASYMMETRY along local Z (centroid - aabb centre) ===")
log("A car has more mass toward the REAR (cabin/boot), so on a correctly")
log("faced shell this should be POSITIVE (mass behind centre, nose at -Z).")
log("")
log(string.format("%-14s %14s %14s %s", "car", "template", "built", "verdict"))
for _, template in ipairs(ReplicatedStorage.CarModels:GetChildren()) do
local id = template.Name
local tA = asymmetry(template, template:GetPivot())
local car = Cars.Get(id)
local bA = nil
if car then
local m = CarManager._buildForDiag(car, Chassis.tuningFor(car))
m.Parent = workspace
m:PivotTo(CFrame.new(0, 5010, 0))
for _ = 1, 3 do
RunService.Heartbeat:Wait()
end
local sh = m:FindFirstChild("Shell")
if sh then
bA = asymmetry(sh, m.PrimaryPart.CFrame)
end
m:Destroy()
end
local verdict = "?"
if tA and bA then
local flipped = (tA > 0) ~= (bA > 0)
verdict = flipped and "rotated by build" or "NOT rotated"
end
log(
string.format(
"%-14s %14s %14s %s",
id,
tA and string.format("%+.3f", tA) or "n/a",
bA and string.format("%+.3f", bA) or "n/a",
verdict
)
)
end
cloneMod:Destroy()
return table.concat(out, "\n")
+190
View File
@@ -0,0 +1,190 @@
--[[
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")
+108
View File
@@ -0,0 +1,108 @@
--[[
diag_shellparts.luau -- dump the actual part inventory of hatch_junk and
hyper_x. diag_nosecue2 found no headlight/taillight/seat on either, so the
naming convention must differ. Read-only: list what IS there, plus a purely
geometric asymmetry cue that needs no naming at all.
Geometric cue (naming-independent): on a car, the passenger cabin /
windscreen sits BEHIND the nose, so the mass distribution along the
longitudinal axis is asymmetric. Cars are also taller at the cabin than at
the nose. Reporting mean height per Z-half lets the shape itself indicate
which end is the bonnet -- the tall half is the cabin/rear-biased end.
]]
local RS = game:GetService("ReplicatedStorage")
local SSS = game:GetService("ServerScriptService")
local SS = game:GetService("ServerStorage")
local TARGETS = { hatch_junk = true, hyper_x = true, rx7_fc = true }
local out = {}
local function log(s)
table.insert(out, s)
end
local shells = {}
for _, root in ipairs({ RS, SSS, SS, workspace }) do
for _, d in ipairs(root:GetDescendants()) do
if d:IsA("Model") and TARGETS[d.Name] and d:FindFirstChild("Wheels") and not shells[d.Name] then
shells[d.Name] = d
end
end
end
for _, name in ipairs({ "rx7_fc", "hatch_junk", "hyper_x" }) do
local shell = shells[name]
if not shell then
log(name .. ": NOT FOUND")
continue
end
local pivotInv = shell:GetPivot():Inverse()
log("=== " .. name .. " ===")
-- Part names outside the Wheels folder, with local Z, sorted front-to-back.
local rows = {}
for _, d in ipairs(shell:GetDescendants()) do
if d:IsA("BasePart") and not d:IsDescendantOf(shell.Wheels) then
local p = pivotInv * d.Position
table.insert(rows, { name = d.Name, z = p.Z, y = p.Y, cls = d.ClassName })
end
end
table.sort(rows, function(a, b)
return a.z < b.z
end)
log(string.format(" %d non-wheel parts (sorted by local Z, -Z first):", #rows))
local shown = 0
for _, r in ipairs(rows) do
if shown < 24 then
log(string.format(" z=%+7.2f y=%+6.2f %-28s %s", r.z, r.y, r.name, r.cls))
shown += 1
end
end
if #rows > 24 then
log(string.format(" ... %d more", #rows - 24))
end
-- Naming-independent shape cue: mean height of parts in each Z half.
local negY, negN, posY, posN = 0, 0, 0, 0
local minZ, maxZ
for _, r in ipairs(rows) do
minZ = minZ and math.min(minZ, r.z) or r.z
maxZ = maxZ and math.max(maxZ, r.z) or r.z
end
local midZ = (minZ and maxZ) and ((minZ + maxZ) / 2) or 0
for _, r in ipairs(rows) do
if r.z < midZ then
negY, negN = negY + r.y, negN + 1
else
posY, posN = posY + r.y, posN + 1
end
end
if negN > 0 and posN > 0 then
local negMean, posMean = negY / negN, posY / posN
log(
string.format(
" shape cue: mean height -Z half = %+.2f (n=%d) +Z half = %+.2f (n=%d)",
negMean,
negN,
posMean,
posN
)
)
local tallEnd = (negMean > posMean) and "-Z" or "+Z"
log(
string.format(
" taller (cabin) end = %s => nose is likely %s",
tallEnd,
(tallEnd == "-Z") and "+Z" or "-Z"
)
)
else
log(" shape cue: unavailable")
end
log("")
end
return table.concat(out, "\n")
+140
View File
@@ -0,0 +1,140 @@
--[[
diag_shellyaw.luau -- measure the ACTUAL shell orientation on LIVE cars.
Why this exists: the previous harness measured Wheel_* positions, which come
from the constant Chassis.WHEEL_MOTOR/WHEELBASE_HALF (5.9). Those are
identical for every car whether or not any fix applied, so the test passed
vacuously. The visible "backwards" car is the COSMETIC SHELL, which is a
separate model attached to the chassis -- that is what must be measured.
Runs against the running game (Server datamodel), inspecting cars that the
real spawn path actually produced. Reports, per live car:
bodyLook : chassis Body forward (should be the car's true nose direction)
shellLook : shell model forward
yawDelta : signed angle between them, degrees. ~0 = aligned,
~180 = shell rendering backwards relative to chassis.
Also dumps the shell's own internal cue (mean Z of its F*/R* wheel labels)
so we can see WHY the solver chose what it chose, and whether
SHELL_YAW_OVERRIDE actually took effect on this car.
]]
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
local function yawBetween(a: Vector3, b: Vector3): number
local av = Vector3.new(a.X, 0, a.Z)
local bv = Vector3.new(b.X, 0, b.Z)
if av.Magnitude < 1e-4 or bv.Magnitude < 1e-4 then
return 0 / 0
end
av, bv = av.Unit, bv.Unit
local dot = math.clamp(av:Dot(bv), -1, 1)
local ang = math.deg(math.acos(dot))
local cross = av:Cross(bv).Y
return (cross < 0) and -ang or ang
end
-- Find live cars anywhere in workspace (folder name varies).
local cars = {}
for _, d in ipairs(workspace:GetDescendants()) do
if d:IsA("Model") and d.Name:match("^Car_") then
cars[#cars + 1] = d
end
end
log(string.format("live Car_* models found: %d", #cars))
if #cars == 0 then
log("")
log("No spawned cars. Drive/spawn a car, or this is the wrong datamodel.")
return table.concat(out, "\n")
end
for _, model in ipairs(cars) do
log("")
log("=== " .. model.Name .. " ===")
local body = model:FindFirstChild("Body")
if not body or not body:IsA("BasePart") then
log(" no Body part -- cannot establish chassis frame")
continue
end
local bodyLook = body.CFrame.LookVector
log(string.format(" bodyLook = (%.2f, %.2f, %.2f)", bodyLook.X, bodyLook.Y, bodyLook.Z))
-- The shell: a Model child that is not the chassis scaffolding.
local shell = nil
for _, c in ipairs(model:GetChildren()) do
if c:IsA("Model") then
shell = c
break
end
end
if not shell then
log(" NO SHELL MODEL as a child -- cosmetic mesh may be parented elsewhere")
local kids = {}
for _, c in ipairs(model:GetChildren()) do
kids[#kids + 1] = c.ClassName .. ":" .. c.Name
end
log(" children: " .. table.concat(kids, ", "))
continue
end
local shellLook = shell:GetPivot().LookVector
log(string.format(" shell = %s", shell.Name))
log(string.format(" shellLook = (%.2f, %.2f, %.2f)", shellLook.X, shellLook.Y, shellLook.Z))
local delta = yawBetween(bodyLook, shellLook)
log(
string.format(
" yawDelta = %.1f deg --> %s",
delta,
(math.abs(delta) < 30) and "ALIGNED"
or (math.abs(math.abs(delta) - 180) < 30) and "BACKWARDS (180 flip)"
or "SKEWED"
)
)
-- Why: the shell's own wheel-label cue, in the shell's local frame.
local wf = shell:FindFirstChild("Wheels")
if wf then
local pivotInv = shell:GetPivot():Inverse()
local fS, fN, rS, rN = 0, 0, 0, 0
for _, d in ipairs(wf:GetDescendants()) do
if d:IsA("BasePart") then
local nm = d.Name:upper()
local z = (pivotInv * d.CFrame).Z
if nm:match("^F") then
fS, fN = fS + z, fN + 1
elseif nm:match("^R") then
rS, rN = rS + z, rN + 1
end
end
end
if fN > 0 and rN > 0 then
log(
string.format(
" shell wheel cue: frontZ=%.2f rearZ=%.2f (labels say nose at %s)",
fS / fN,
rS / rN,
(fS / fN < rS / rN) and "-Z" or "+Z"
)
)
else
log(string.format(" shell wheel cue: unusable (F parts=%d, R parts=%d)", fN, rN))
end
else
log(" shell has no Wheels folder -- no cue available")
end
end
return table.concat(out, "\n")
+95
View File
@@ -0,0 +1,95 @@
--[[
diag_shellyaw2.luau -- corrected. The first attempt looked in the wrong
place (CarShells/Shells/Cars) and then measured the CHASSIS wheels, so it
reported an identical -11.80 wheelbase for every car and proved nothing.
Shells actually live in ReplicatedStorage.CarModels[car.Id] (CarManager
line 347). This inspects those templates directly and reports the exact
cue the auto-aligner uses, plus the resulting yaw.
]]
local ReplicatedStorage = game:GetService("ReplicatedStorage")
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
local models = ReplicatedStorage:FindFirstChild("CarModels")
if not models then
log("ReplicatedStorage.CarModels: MISSING -> every car uses the procedural")
log("CarBody shell, which is authored nose-toward -Z and cannot be backwards.")
return table.concat(out, "\n")
end
log("ReplicatedStorage.CarModels children:")
for _, t in ipairs(models:GetChildren()) do
log(string.format(" %-14s (%s)", t.Name, t.ClassName))
end
log("")
for _, template in ipairs(models:GetChildren()) do
if template:IsA("Model") then
local wf = template:FindFirstChild("Wheels")
if not wf then
log(
string.format(
"%-14s Wheels folder MISSING -> shellYaw = 0 (fails soft, keeps authored yaw)",
template.Name
)
)
else
local pivotInv = template:GetPivot():Inverse()
local fSum, fN, rSum, rN = Vector3.zero, 0, Vector3.zero, 0
local names = {}
for _, w in ipairs(wf:GetChildren()) do
table.insert(names, w.Name)
local part = w:IsA("BasePart") and w or w:FindFirstChildWhichIsA("BasePart", true)
if part then
local h = w.Name:sub(1, 1):upper()
local lp = pivotInv * part.Position
if h == "F" then
fSum, fN = fSum + lp, fN + 1
elseif h == "R" then
rSum, rN = rSum + lp, rN + 1
end
end
end
if fN > 0 and rN > 0 then
local nose = (fSum / fN) - (rSum / rN)
local usable = (math.abs(nose.X) + math.abs(nose.Z)) > 0.05
local yaw = usable and -math.atan2(nose.X, -nose.Z) or 0
log(
string.format(
"%-14s [%s] F=%d R=%d nose=(%.2f,%.2f,%.2f) -> yaw %+.1f deg %s",
template.Name,
table.concat(names, ","),
fN,
rN,
nose.X,
nose.Y,
nose.Z,
math.deg(yaw),
usable and "" or "*** DEGENERATE -> yaw 0 ***"
)
)
else
log(
string.format(
"%-14s [%s] F=%d R=%d -> *** NO F/R CUE, shellYaw = 0, stays as authored ***",
template.Name,
table.concat(names, ","),
fN,
rN
)
)
end
end
end
end
return table.concat(out, "\n")
+144
View File
@@ -0,0 +1,144 @@
--[[
diag_solveryaw.luau -- does the shell yaw solver actually fire for
hatch_junk / hyper_x, and what does it produce?
Hypothesis under test: the solver derives "nose" from the F/R wheel labels,
so for a shell whose F wheels sit at +Z it should compute a 180-degree yaw
and rotate the shell to face -Z. If that is happening, those two cars would
look CORRECT -- but they look backwards. So either the solver is not being
reached, or it computes something other than 180.
CACHE GUARD FIRST. require() caches for the whole Studio session, so a
build test can silently run OLD code. Before trusting anything below, the
script re-reads Chassis.DRIVE_SIGN via require() and compares it to the
.Source text. If they disagree, the cache is stale and the script ABORTS
rather than reporting a number that means nothing.
]]
local RS = game:GetService("ReplicatedStorage")
local SSS = game:GetService("ServerScriptService")
local out = {}
local function log(s)
table.insert(out, s)
end
local function find(name)
for _, root in ipairs({ SSS, RS, game:GetService("ServerStorage"), workspace }) do
local m = root:FindFirstChild(name, true)
if m and m:IsA("ModuleScript") then
return m
end
end
return nil
end
-- ---- cache guard -------------------------------------------------------
local chMod = find("Chassis")
if not chMod then
return "FATAL: Chassis not found"
end
local srcHasNeg1 = chMod.Source:find("DRIVE_SIGN = -1", 1, true) ~= nil
local okReq, Chassis = pcall(require, chMod)
if not okReq then
return "FATAL: require(Chassis) failed: " .. tostring(Chassis)
end
local liveNeg1 = (Chassis.DRIVE_SIGN == -1)
log(
string.format(
"cache guard: source DRIVE_SIGN=-1? %s require() DRIVE_SIGN=%s",
tostring(srcHasNeg1),
tostring(Chassis.DRIVE_SIGN)
)
)
if srcHasNeg1 ~= liveNeg1 then
log("")
log(">>> ABORT: require() cache disagrees with source. Any build measured now")
log(" would use STALE code. Restart Studio (or reopen the place) and re-run.")
return table.concat(out, "\n")
end
log("cache guard: PASS (require matches source)")
log("")
-- ---- what does the solver compute? -------------------------------------
-- Recompute the solver's own input the same way the source does: nose is the
-- F-centroid minus R-centroid, in shell-local space; yaw = -atan2(nose.X, -nose.Z).
local TARGETS = { "rx7_fc", "silv_s15", "gtr_r34", "hatch_junk", "hyper_x" }
local shells = {}
for _, root in ipairs({ RS, SSS, game:GetService("ServerStorage"), workspace }) do
for _, d in ipairs(root:GetDescendants()) do
if d:IsA("Model") and d:FindFirstChild("Wheels") and not shells[d.Name] then
for _, t in ipairs(TARGETS) do
if d.Name == t then
shells[d.Name] = d
end
end
end
end
end
log(string.format("%-11s %-12s %-14s %s", "shell", "nose.Z", "solver yaw", "meaning"))
log(string.rep("-", 74))
for _, name in ipairs(TARGETS) do
local shell = shells[name]
if not shell then
log(string.format("%-11s NOT FOUND", name))
continue
end
local pivotInv = shell:GetPivot():Inverse()
local wheels = shell:FindFirstChild("Wheels")
local fSum, fN, rSum, rN = Vector3.zero, 0, 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 lp = pivotInv * (sum / n)
local head = w.Name:sub(1, 1):upper()
if head == "F" then
fSum, fN = fSum + lp, fN + 1
elseif head == "R" then
rSum, rN = rSum + lp, rN + 1
end
end
end
if fN == 0 or rN == 0 then
log(string.format("%-11s no F/R labels", name))
continue
end
local nose = (fSum / fN) - (rSum / rN)
local yaw = -math.atan2(nose.X, -nose.Z)
local deg = math.deg(yaw)
local meaning
if math.abs(deg) < 15 then
meaning = "no rotation applied"
elseif math.abs(math.abs(deg) - 180) < 15 then
meaning = "180 flip -> solver DOES correct this shell"
else
meaning = "partial rotation"
end
log(
string.format(
"%-11s %-12s %-14s %s",
name,
string.format("%+.1f", nose.Z),
string.format("%+.1f deg", deg),
meaning
)
)
end
log("")
log("If the two reported-backwards shells show a 180 flip here, the solver is")
log("already correcting them in theory -- so the fault is that this code path")
log("is not reached at build time, NOT the maths. That distinction decides the fix.")
return table.concat(out, "\n")
+101
View File
@@ -0,0 +1,101 @@
--[[
diag_stale.luau -- is the live CarManager module the source I pushed?
diag_wheelroot2 failed with "_buildForDiag is nil" even though the file on
disk defines it at line 988 and push_module reported
assembled=39591 expected=39591 match=true.
So either (a) the ModuleScript I am cloning is not the one I pushed to, or
(b) there is more than one CarManager, or (c) the clone-and-require trick
is returning a cached table from the ORIGINAL require rather than
re-executing the clone's source.
(c) is the important one: if require(clone) hands back the cached original
table, then every "built car" measurement in this session came from the
OLD module, and all of those results are void.
Checks, with no assumptions:
1. Every ModuleScript named CarManager anywhere in the DataModel, with
its source length and whether its text contains _buildForDiag.
2. What require() actually returns for the original and for a fresh
clone -- key list and identity comparison.
]]
local ServerScriptService = game:GetService("ServerScriptService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
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
log("=== every CarManager ModuleScript in the DataModel ===")
local found = {}
for _, svc in ipairs({ ServerScriptService, ReplicatedStorage, game:GetService("ServerStorage"), workspace }) do
for _, d in ipairs(svc:GetDescendants()) do
if d:IsA("ModuleScript") and d.Name == "CarManager" then
found[#found + 1] = d
local src = ""
local okSrc = pcall(function()
src = d.Source
end)
log(
string.format(
" %-58s len=%s has _buildForDiag=%s",
d:GetFullName(),
okSrc and tostring(#src) or "<unreadable>",
okSrc and tostring(src:find("_buildForDiag") ~= nil) or "?"
)
)
end
end
end
log(string.format(" total found: %d", #found))
if #found == 0 then
return table.concat(out, "\n")
end
local target = found[1]
log("")
log("=== what require() returns ===")
local okA, A = pcall(require, target)
log(" require(original): ok=" .. tostring(okA))
if okA and typeof(A) == "table" then
local keys = {}
for k in pairs(A) do
keys[#keys + 1] = tostring(k)
end
table.sort(keys)
log(" keys: " .. table.concat(keys, ", "))
log(" _buildForDiag = " .. typeof(A._buildForDiag))
end
local clone = target:Clone()
clone.Parent = target.Parent
local okB, B = pcall(require, clone)
log(" require(clone): ok=" .. tostring(okB))
if okB and typeof(B) == "table" then
local keys = {}
for k in pairs(B) do
keys[#keys + 1] = tostring(k)
end
table.sort(keys)
log(" keys: " .. table.concat(keys, ", "))
log(" _buildForDiag = " .. typeof(B._buildForDiag))
end
if okA and okB then
log(
" clone table IS original table: "
.. tostring(rawequal(A, B))
.. " <- if true, the clone trick does NOT reload source"
)
end
clone:Destroy()
return table.concat(out, "\n")
+204
View File
@@ -0,0 +1,204 @@
--[[
diag_steer2.luau -- WHY does the steering servo not rotate?
diag_steering showed TargetAngle -14..-26 deg with CurrentAngle stuck near
0.3 deg. This isolates the cause by removing one load at a time:
A. HOISTED -- body anchored, wheels in free air. No tyre load at all.
If the servo still will not turn, the fault is the
constraint itself, not grip.
B. GROUND, STATIC -- resting on the pad, no throttle.
C. GROUND, ROLLING -- coasting, no drive torque (drive is what saturates
the friction circle).
D. TORQUE SWEEP -- is 1e6 actually enough?
]]
local ServerScriptService = game:GetService("ServerScriptService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
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
local Shared = ReplicatedStorage:FindFirstChild("Shared")
local Cars = require(Shared.Cars)
local Chassis = require(Shared.Chassis)
local Server = ServerScriptService:FindFirstChild("Server", true)
local CarManager = require(Server.CarManager)
local pad = Instance.new("Part")
pad.Name = "DiagPad2"
pad.Size = Vector3.new(4000, 8, 4000)
pad.Position = Vector3.new(0, 5000, 0)
pad.Anchored = true
pad.Material = Enum.Material.Concrete
pad.Parent = workspace
local padTop = pad.Position.Y + 4
local car = Cars.Get("gtr_r34")
local tuning = Chassis.tuningFor(car)
local function build()
local m = CarManager._buildForDiag(car, tuning)
m.Parent = workspace
m:PivotTo(CFrame.new(0, padTop + 3, 0))
return m
end
local function partsOf(model)
local servos, motors = {}, {}
for _, d in ipairs(model:GetDescendants()) do
if d:IsA("HingeConstraint") then
if d.Name == Chassis.STEER_SERVO_NAME then
table.insert(servos, d)
elseif d.Name == Chassis.WHEEL_MOTOR_NAME then
table.insert(motors, d)
end
end
end
return servos, motors
end
local LOCK = tuning.MaxSteerAngle
log(string.format("MaxSteerAngle %.1f deg | ServoMaxTorque 1e6 | AngularSpeed %.1f rad/s", LOCK, tuning.SteerSpeed))
log("")
-- Report the hinge axes once: a mis-set axis would make the servo yaw about
-- the wrong vector and look exactly like "no steering".
do
local m = build()
local servos = partsOf(m)
for i, s in ipairs(servos) do
local a0 = s.Attachment0
local a1 = s.Attachment1
log(
string.format(
"servo %d axis0 (%.2f,%.2f,%.2f) axis1 (%.2f,%.2f,%.2f) | A0 on %s, A1 on %s",
i,
a0.WorldAxis.X,
a0.WorldAxis.Y,
a0.WorldAxis.Z,
a1.WorldAxis.X,
a1.WorldAxis.Y,
a1.WorldAxis.Z,
a0.Parent.Name,
a1.Parent.Name
)
)
end
m:Destroy()
log("")
end
-- A. HOISTED ---------------------------------------------------------------
do
local m = build()
local body = m.PrimaryPart
m:PivotTo(CFrame.new(0, padTop + 40, 0)) -- well clear of the pad
body.Anchored = true
local servos = partsOf(m)
for _ = 1, 30 do
RunService.Heartbeat:Wait()
end
for _, s in ipairs(servos) do
s.TargetAngle = -LOCK
end
local trace = {}
for step = 1, 120 do
if step % 20 == 0 then
table.insert(trace, string.format("%.1f", servos[1].CurrentAngle))
end
RunService.Heartbeat:Wait()
end
log(string.format("A. HOISTED (no tyre load): target %.1f -> CurrentAngle %s", -LOCK, table.concat(trace, ", ")))
log(string.format(" final err %.2f deg", math.abs(servos[1].CurrentAngle - -LOCK)))
m:Destroy()
end
-- B. GROUND, STATIC --------------------------------------------------------
do
local m = build()
local servos = partsOf(m)
for _ = 1, 60 do
RunService.Heartbeat:Wait()
end
for _, s in ipairs(servos) do
s.TargetAngle = -LOCK
end
local trace = {}
for step = 1, 120 do
if step % 20 == 0 then
table.insert(trace, string.format("%.1f", servos[1].CurrentAngle))
end
RunService.Heartbeat:Wait()
end
log(string.format("B. GROUND STATIC: target %.1f -> CurrentAngle %s", -LOCK, table.concat(trace, ", ")))
log(string.format(" final err %.2f deg", math.abs(servos[1].CurrentAngle - -LOCK)))
m:Destroy()
end
-- C. GROUND, COASTING (no drive torque) ------------------------------------
do
local m = build()
local body = m.PrimaryPart
local servos, motors = partsOf(m)
for _ = 1, 60 do
RunService.Heartbeat:Wait()
end
-- Push to a moderate speed, then cut drive entirely and steer.
body.AssemblyLinearVelocity = body.CFrame.LookVector * 60
for _, mo in ipairs(motors) do
mo.MotorMaxTorque = 0 -- freewheel: friction circle fully available for cornering
mo.AngularVelocity = 0
end
for _, s in ipairs(servos) do
s.TargetAngle = -LOCK
end
local trace, yawTrace = {}, {}
for step = 1, 150 do
if step % 30 == 0 then
table.insert(trace, string.format("%.1f", servos[1].CurrentAngle))
table.insert(yawTrace, string.format("%.3f", body.AssemblyAngularVelocity.Y))
end
RunService.Heartbeat:Wait()
end
log(string.format("C. GROUND COASTING @60: CurrentAngle %s", table.concat(trace, ", ")))
log(string.format(" yawRate %s", table.concat(yawTrace, ", ")))
m:Destroy()
end
-- D. TORQUE SWEEP (ground, static) -----------------------------------------
log("")
log("D. ServoMaxTorque sweep (ground, static, full lock, 1.5 s):")
for _, tq in ipairs({ 1e5, 1e6, 1e7, 1e8, 1e9 }) do
local m = build()
local servos = partsOf(m)
for _ = 1, 45 do
RunService.Heartbeat:Wait()
end
for _, s in ipairs(servos) do
s.ServoMaxTorque = tq
s.TargetAngle = -LOCK
end
for _ = 1, 90 do
RunService.Heartbeat:Wait()
end
log(
string.format(
" torque %10.0e -> CurrentAngle %6.1f (err %5.1f)",
tq,
servos[1].CurrentAngle,
math.abs(servos[1].CurrentAngle + LOCK)
)
)
m:Destroy()
end
pad:Destroy()
return table.concat(out, "\n")
+188
View File
@@ -0,0 +1,188 @@
--[[
diag_steer3.luau -- WHAT is holding the knuckle rigid?
diag_steer2 proved the servo will not rotate even hoisted with zero tyre
load and ServoMaxTorque up to 1e9, with both hinge axes correctly +Y.
That means the knuckle is rigidly bonded to the hub by something other
than the servo.
Decisive test: parts joined by a WELD share an AssemblyRootPart. Parts
joined by a moving CONSTRAINT do not. So if Hub_FL and Knuckle_FL report
the same AssemblyRootPart, a weld is bypassing the servo.
Also builds an isolated 2-part servo to confirm the engine/API itself
behaves, ruling out a bad assumption about HingeConstraint.
]]
local ServerScriptService = game:GetService("ServerScriptService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
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
-- 1. ISOLATED SERVO --------------------------------------------------------
-- Two free parts, one servo, nothing else. If this turns, the API is fine
-- and the fault is in how the car is assembled.
do
local folder = Instance.new("Folder")
folder.Name = "IsolatedServoTest"
folder.Parent = workspace
local base = Instance.new("Part")
base.Size = Vector3.new(4, 1, 4)
base.Position = Vector3.new(0, 5100, 0)
base.Anchored = true
base.Parent = folder
local arm = Instance.new("Part")
arm.Size = Vector3.new(1, 1, 6)
arm.Position = Vector3.new(0, 5102, 0)
arm.Parent = folder
local VERT = CFrame.Angles(0, 0, math.rad(90))
local a0 = Instance.new("Attachment")
a0.CFrame = VERT
a0.Parent = base
local a1 = Instance.new("Attachment")
a1.CFrame = VERT
a1.Parent = arm
local s = Instance.new("HingeConstraint")
s.Attachment0 = a0
s.Attachment1 = a1
s.ActuatorType = Enum.ActuatorType.Servo
s.ServoMaxTorque = 1e6
s.AngularSpeed = 6
s.LimitsEnabled = true
s.LowerAngle = -35
s.UpperAngle = 35
s.TargetAngle = -30
s.Parent = base
for _ = 1, 90 do
RunService.Heartbeat:Wait()
end
log(
string.format(
"1. ISOLATED servo: target -30 -> CurrentAngle %.2f (%s)",
s.CurrentAngle,
math.abs(s.CurrentAngle + 30) < 5 and "WORKS" or "BROKEN"
)
)
folder:Destroy()
end
-- 2. ASSEMBLY TOPOLOGY OF A REAL CAR ---------------------------------------
local Shared = ReplicatedStorage:FindFirstChild("Shared")
local Cars = require(Shared.Cars)
local Chassis = require(Shared.Chassis)
local Server = ServerScriptService:FindFirstChild("Server", true)
local CarManager = require(Server.CarManager)
local pad = Instance.new("Part")
pad.Size = Vector3.new(2000, 8, 2000)
pad.Position = Vector3.new(0, 5000, 0)
pad.Anchored = true
pad.Parent = workspace
local car = Cars.Get("gtr_r34")
local tuning = Chassis.tuningFor(car)
local model = CarManager._buildForDiag(car, tuning)
model.Parent = workspace
model:PivotTo(CFrame.new(0, 5007, 0))
for _ = 1, 60 do
RunService.Heartbeat:Wait()
end
local function get(name)
for _, d in ipairs(model:GetDescendants()) do
if d:IsA("BasePart") and d.Name == name then
return d
end
end
return nil
end
local body = model.PrimaryPart
local hubFL = get("Hub_FL")
local knuckleFL = get("Knuckle_FL")
local wheelFL = get("Wheel_FL")
log("")
log("2. ASSEMBLY ROOTS (same root == rigidly welded together):")
local function rootName(p)
if not p then
return "<missing>"
end
local r = p.AssemblyRootPart
return r and r:GetFullName():gsub("^.*%.", "") or "<none>"
end
log(" Body root:", rootName(body))
log(" Hub_FL root:", rootName(hubFL))
log(" Knuckle_FL root:", rootName(knuckleFL))
log(" Wheel_FL root:", rootName(wheelFL))
if hubFL and knuckleFL then
local same = hubFL.AssemblyRootPart == knuckleFL.AssemblyRootPart
log(
" >>> Hub and Knuckle share an assembly:",
tostring(same),
same and "<-- WELDED, SERVO BYPASSED" or "(ok, servo is free)"
)
end
-- 3. EVERY joint touching those parts --------------------------------------
log("")
log("3. ALL joints touching Hub_FL / Knuckle_FL / Wheel_FL:")
local targets = { [hubFL] = "Hub_FL", [knuckleFL] = "Knuckle_FL", [wheelFL] = "Wheel_FL" }
for _, d in ipairs(model:GetDescendants()) do
local p0, p1, kind
if d:IsA("WeldConstraint") then
p0, p1, kind = d.Part0, d.Part1, "WeldConstraint"
elseif d:IsA("Weld") or d:IsA("Motor6D") then
p0, p1, kind = d.Part0, d.Part1, d.ClassName
elseif d:IsA("Constraint") then
local a0, a1 = d.Attachment0, d.Attachment1
p0 = a0 and a0.Parent or nil
p1 = a1 and a1.Parent or nil
kind = d.ClassName .. "/" .. d.Name
end
if p0 and p1 and (targets[p0] or targets[p1]) then
log(string.format(" %-28s %s <-> %s", kind, p0.Name, p1.Name))
end
end
-- 4. Servo runtime state ---------------------------------------------------
log("")
log("4. SERVO RUNTIME STATE:")
for _, d in ipairs(model:GetDescendants()) do
if d:IsA("HingeConstraint") and d.Name == Chassis.STEER_SERVO_NAME then
log(
string.format(
" %s Enabled=%s Active=%s Actuator=%s Torque=%.0e Speed=%.1f Limits=%s [%.1f..%.1f] Target=%.1f Current=%.2f",
d.Parent.Name,
tostring(d.Enabled),
tostring(d.Active),
d.ActuatorType.Name,
d.ServoMaxTorque,
d.AngularSpeed,
tostring(d.LimitsEnabled),
d.LowerAngle,
d.UpperAngle,
d.TargetAngle,
d.CurrentAngle
)
)
end
end
model:Destroy()
pad:Destroy()
return table.concat(out, "\n")
+153
View File
@@ -0,0 +1,153 @@
--[[
diag_steer4.luau -- isolate the real constraint on steering.
Prior runs had a CONFOUND: every hoisted/static test commanded exactly
-MaxSteerAngle, which is exactly the servo's LowerAngle limit. Sitting a
servo precisely on its own limit can deadlock it. The isolated 2-part
servo (target -30, limits +/-35) worked perfectly, so the API is fine.
This matrix commands -20 deg (well INSIDE the +/-35.08 limits) and varies
one thing at a time.
]]
local ServerScriptService = game:GetService("ServerScriptService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
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
local Shared = ReplicatedStorage:FindFirstChild("Shared")
local Cars = require(Shared.Cars)
local Chassis = require(Shared.Chassis)
local Server = ServerScriptService:FindFirstChild("Server", true)
local CarManager = require(Server.CarManager)
local pad = Instance.new("Part")
pad.Size = Vector3.new(3000, 8, 3000)
pad.Position = Vector3.new(0, 5000, 0)
pad.Anchored = true
pad.Material = Enum.Material.Concrete
pad.Parent = workspace
local padTop = pad.Position.Y + 4
local car = Cars.Get("gtr_r34")
local tuning = Chassis.tuningFor(car)
local LIMIT = tuning.MaxSteerAngle
local INSIDE = -20 -- comfortably inside +/-35.08
local function build(y)
local m = CarManager._buildForDiag(car, tuning)
m.Parent = workspace
m:PivotTo(CFrame.new(0, y, 0))
return m
end
local function servosOf(model)
local servos, motors = {}, {}
for _, d in ipairs(model:GetDescendants()) do
if d:IsA("HingeConstraint") then
if d.Name == Chassis.STEER_SERVO_NAME then
table.insert(servos, d)
elseif d.Name == Chassis.WHEEL_MOTOR_NAME then
table.insert(motors, d)
end
end
end
return servos, motors
end
-- Run one case: returns the CurrentAngle trace.
local function runCase(label, opts)
local m = build(opts.hoist and (padTop + 40) or (padTop + 3))
local body = m.PrimaryPart
local servos, motors = servosOf(m)
if opts.hoist then
body.Anchored = true
end
if opts.noWheel then
for _, d in ipairs(m:GetDescendants()) do
if d:IsA("BasePart") and d.Name:sub(1, 6) == "Wheel_" then
d:Destroy()
end
end
end
if opts.motorOff then
for _, mo in ipairs(motors) do
mo.Enabled = false
end
end
if opts.noLimits then
for _, s in ipairs(servos) do
s.LimitsEnabled = false
end
end
for _ = 1, 60 do
RunService.Heartbeat:Wait()
end
if opts.speed then
body.AssemblyLinearVelocity = body.CFrame.LookVector * opts.speed
for _, mo in ipairs(motors) do
mo.MotorMaxTorque = 0
end
end
for _, s in ipairs(servos) do
s.TargetAngle = opts.target
end
local trace = {}
for step = 1, 120 do
if opts.speed then
for _, s in ipairs(servos) do
s.TargetAngle = opts.target
end
end
if step % 24 == 0 then
trace[#trace + 1] = string.format("%.1f", servos[1].CurrentAngle)
end
RunService.Heartbeat:Wait()
end
local final = servos[1].CurrentAngle
local ok = math.abs(final - opts.target) < 5
log(
string.format(
"%-42s target %6.1f -> [%s] final %6.2f %s",
label,
opts.target,
table.concat(trace, " "),
final,
ok and "*** TURNS ***" or "stuck"
)
)
m:Destroy()
end
log(
string.format("limits +/-%.2f deg | INSIDE target %.1f | AngularSpeed %.1f rad/s", LIMIT, INSIDE, tuning.SteerSpeed)
)
log("")
runCase("A1 hoisted, target INSIDE limits", { hoist = true, target = INSIDE })
runCase("A2 hoisted, target AT limit", { hoist = true, target = -LIMIT })
runCase("A3 hoisted, LimitsEnabled=false", { hoist = true, target = INSIDE, noLimits = true })
runCase("A4 hoisted, wheels removed", { hoist = true, target = INSIDE, noWheel = true })
runCase("A5 hoisted, wheel motors disabled", { hoist = true, target = INSIDE, motorOff = true })
log("")
runCase("B1 ground static, target INSIDE", { target = INSIDE })
runCase("B2 ground static, motors disabled", { target = INSIDE, motorOff = true })
log("")
runCase("C1 ground coasting @60, INSIDE", { target = INSIDE, speed = 60 })
runCase("C2 ground coasting @120, INSIDE", { target = INSIDE, speed = 120 })
pad:Destroy()
return table.concat(out, "\n")
+179
View File
@@ -0,0 +1,179 @@
--[[
diag_steer5.luau -- find a FIX for the servo/motor conflict.
Established by diag_steer4: an ENABLED WheelMotor hinge locks the knuckle
so the steer servo cannot yaw it. Disabling the motor (or deleting the
wheel) restores perfect tracking (-19.99 of -20.0). MotorMaxTorque is 0 at
build time, so this is NOT a torque contest -- it is a degenerate
constraint configuration.
Suspicion: Hub, Knuckle and Wheel are all built at the SAME CFrame, and
the motor's two attachments are both identity CFrames, so every attachment
sits at the same world point. Two hinges sharing a point with perpendicular
axes is ill-conditioned for the solver.
Each variant below changes ONE thing and re-measures.
]]
local ServerScriptService = game:GetService("ServerScriptService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
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
local Shared = ReplicatedStorage:FindFirstChild("Shared")
local Cars = require(Shared.Cars)
local Chassis = require(Shared.Chassis)
local Server = ServerScriptService:FindFirstChild("Server", true)
local CarManager = require(Server.CarManager)
local pad = Instance.new("Part")
pad.Size = Vector3.new(3000, 8, 3000)
pad.Position = Vector3.new(0, 5000, 0)
pad.Anchored = true
pad.Material = Enum.Material.Concrete
pad.Parent = workspace
local padTop = pad.Position.Y + 4
local car = Cars.Get("gtr_r34")
local tuning = Chassis.tuningFor(car)
local TARGET = -20
local function parts(model)
local servos, motors, wheels, knuckles = {}, {}, {}, {}
for _, d in ipairs(model:GetDescendants()) do
if d:IsA("HingeConstraint") then
if d.Name == Chassis.STEER_SERVO_NAME then
table.insert(servos, d)
elseif d.Name == Chassis.WHEEL_MOTOR_NAME then
table.insert(motors, d)
end
elseif d:IsA("BasePart") then
if d.Name:sub(1, 6) == "Wheel_" then
table.insert(wheels, d)
elseif d.Name:sub(1, 8) == "Knuckle_" then
table.insert(knuckles, d)
end
end
end
return servos, motors, wheels, knuckles
end
-- mutate: applied after build, before settling. drive: applied after settling.
local function variant(label, mutate, drive)
local m = CarManager._buildForDiag(car, tuning)
m.Parent = workspace
m:PivotTo(CFrame.new(0, padTop + 3, 0))
local servos, motors, wheels, knuckles = parts(m)
if mutate then
mutate(servos, motors, wheels, knuckles, m)
end
for _ = 1, 60 do
RunService.Heartbeat:Wait()
end
local body = m.PrimaryPart
body.AssemblyLinearVelocity = body.CFrame.LookVector * 80
if drive then
drive(servos, motors, wheels, knuckles, m)
end
for _, s in ipairs(servos) do
s.TargetAngle = TARGET
end
local trace, yaw = {}, {}
for step = 1, 120 do
for _, s in ipairs(servos) do
s.TargetAngle = TARGET
end
if step % 24 == 0 then
trace[#trace + 1] = string.format("%.1f", servos[1].CurrentAngle)
yaw[#yaw + 1] = string.format("%.2f", body.AssemblyAngularVelocity.Y)
end
RunService.Heartbeat:Wait()
end
local final = servos[1].CurrentAngle
local ok = math.abs(final - TARGET) < 5
log(
string.format(
"%-40s [%s] final %6.2f yaw[%s] %s",
label,
table.concat(trace, " "),
final,
table.concat(yaw, " "),
ok and "*** STEERS ***" or "stuck"
)
)
m:Destroy()
end
log(string.format("target %.1f deg, rolling at 80 studs/s, motors at build-default torque", TARGET))
log("")
variant("V0 baseline (unmodified)", nil, nil)
variant("V1 motor ActuatorType = None", function(_, motors)
for _, mo in ipairs(motors) do
mo.ActuatorType = Enum.ActuatorType.None
end
end)
variant("V2 motor attachments offset 0.01 X", function(_, motors)
for _, mo in ipairs(motors) do
mo.Attachment0.CFrame = mo.Attachment0.CFrame * CFrame.new(0.01, 0, 0)
end
end)
variant("V3 knuckle mass x20 (density)", function(_, _, _, knuckles)
for _, k in ipairs(knuckles) do
k.CustomPhysicalProperties = PhysicalProperties.new(20, 0.3, 0.5, 1, 1)
end
end)
variant("V4 wheel Massless=true", function(_, _, wheels)
for _, w in ipairs(wheels) do
w.Massless = true
end
end)
variant("V5 servo torque 1e9 + speed 30", function(servos)
for _, s in ipairs(servos) do
s.ServoMaxTorque = 1e9
s.AngularSpeed = 30
end
end)
variant("V6 motor torque 0 held every frame", nil, function(_, motors)
for _, mo in ipairs(motors) do
mo.MotorMaxTorque = 0
mo.AngularVelocity = 0
end
end)
variant("V7 front motors disabled only", function(_, motors)
-- rear-wheel drive: only the steered corners freewheel
for _, mo in ipairs(motors) do
local n = mo.Parent and mo.Parent.Name or ""
if n:find("F") then
mo.Enabled = false
end
end
end)
variant("V8 V1 + V5 combined", function(servos, motors)
for _, mo in ipairs(motors) do
mo.ActuatorType = Enum.ActuatorType.None
end
for _, s in ipairs(servos) do
s.ServoMaxTorque = 1e9
s.AngularSpeed = 30
end
end)
pad:Destroy()
return table.concat(out, "\n")
+203
View File
@@ -0,0 +1,203 @@
--[[
diag_steering.luau -- measure WHY the car will not turn.
Builds a real car through CarManager, gives it a straight test surface,
then drives it at full lock and measures what actually happens:
* masses (body vs unsprung vs ballast)
* servo TargetAngle vs CurrentAngle under load
* achieved yaw rate and turning radius vs the Ackermann prediction
* lateral slip at each wheel
Everything is measured in-engine; nothing here is assumed.
]]
local ServerScriptService = game:GetService("ServerScriptService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
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
local Shared = ReplicatedStorage:FindFirstChild("Shared")
local Cars = require(Shared.Cars)
local Chassis = require(Shared.Chassis)
local Server = ServerScriptService:FindFirstChild("Server", true)
local CarManager = require(Server.CarManager)
-- Flat, generous test pad well away from the map.
local pad = Instance.new("Part")
pad.Name = "DiagPad"
pad.Size = Vector3.new(4000, 8, 4000)
pad.Position = Vector3.new(0, 5000, 0)
pad.Anchored = true
pad.Material = Enum.Material.Concrete
pad.Parent = workspace
local padTop = pad.Position.Y + 4
log(
"pad friction:",
string.format("%.2f", pad.CurrentPhysicalProperties.Friction),
" frictionWeight:",
string.format("%.2f", pad.CurrentPhysicalProperties.FrictionWeight)
)
local carId = "gtr_r34"
local car = Cars.Get(carId)
local tuning = Chassis.tuningFor(car)
log("car:", carId, "Handling:", car.Stats.Handling, "Speed:", car.Stats.Speed)
log(
string.format(
"tuning MaxSteerAngle %.1f deg, SteerSpeed %.1f, WheelFriction %.2f, MaxSpeed %.0f",
tuning.MaxSteerAngle,
tuning.SteerSpeed,
tuning.WheelFriction,
tuning.MaxSpeed
)
)
-- Build the car exactly as the game does.
local model = CarManager._buildForDiag and CarManager._buildForDiag(car, tuning) or nil
if not model then
-- fall back: use the public spawn path into the pad area
log("NOTE: no diag hook; using CarManager.SpawnCarAt if present")
end
if not model then
log("FATAL: cannot build a car model headlessly -- need a build hook")
return table.concat(out, "\n")
end
model.Parent = workspace
model:PivotTo(CFrame.new(0, padTop + 4, 0))
local body = model.PrimaryPart
local function findAll(name)
local t = {}
for _, d in ipairs(model:GetDescendants()) do
if d.Name == name or (d:IsA("BasePart") and d.Name:sub(1, #name) == name) then
table.insert(t, d)
end
end
return t
end
-- MASS BREAKDOWN -----------------------------------------------------------
local total, sprung, unsprung = 0, 0, 0
for _, d in ipairs(model:GetDescendants()) do
if d:IsA("BasePart") and not d.Massless then
local m = d:GetMass()
total += m
if d.Name:sub(1, 5) == "Wheel" or d.Name:sub(1, 3) == "Hub" or d.Name:sub(1, 7) == "Knuckle" then
unsprung += m
else
sprung += m
end
end
end
log(
string.format(
"MASS total %.1f | sprung %.1f | unsprung %.1f (%.0f%% unsprung)",
total,
sprung,
unsprung,
100 * unsprung / math.max(total, 1)
)
)
local wheels = findAll("Wheel_")
local servos = {}
for _, d in ipairs(model:GetDescendants()) do
if d:IsA("HingeConstraint") and d.Name == Chassis.STEER_SERVO_NAME then
table.insert(servos, d)
end
end
local motors = {}
for _, d in ipairs(model:GetDescendants()) do
if d:IsA("HingeConstraint") and d.Name == Chassis.WHEEL_MOTOR_NAME then
table.insert(motors, d)
end
end
log("found", #wheels, "wheels,", #servos, "servos,", #motors, "motors")
if #wheels > 0 then
local w = wheels[1]
local p = w.CurrentPhysicalProperties
log(string.format("wheel friction %.2f weight %.2f | mass %.2f", p.Friction, p.FrictionWeight, w:GetMass()))
end
-- Let it settle on the pad.
for _ = 1, 90 do
RunService.Heartbeat:Wait()
end
log(string.format("settled: body Y %.2f, vel %.2f", body.Position.Y, body.AssemblyLinearVelocity.Magnitude))
-- DRIVE TEST: full throttle, full right lock --------------------------------
local samples = {}
local t0 = os.clock()
local startCF = body.CFrame
local maxServoErr, servoErrSum, servoN = 0, 0, 0
for step = 1, 300 do
local speed = body.AssemblyLinearVelocity.Magnitude
local speedFrac = math.clamp(speed / tuning.MaxSpeed, 0, 1)
local lockScale = 1 - 0.6 * speedFrac
local targetAngle = Chassis.STEER_SIGN * 1 * tuning.MaxSteerAngle * lockScale
for _, s in ipairs(servos) do
s.TargetAngle = targetAngle
local err = math.abs(s.CurrentAngle - targetAngle)
maxServoErr = math.max(maxServoErr, err)
servoErrSum += err
servoN += 1
end
for _, m in ipairs(motors) do
m.AngularVelocity = Chassis.DRIVE_SIGN * tuning.MaxWheelAngularVelocity
m.MotorMaxTorque = tuning.MotorMaxTorque
end
if step % 30 == 0 then
local yawRate = body.AssemblyAngularVelocity.Y
table.insert(samples, {
t = os.clock() - t0,
speed = speed,
yaw = yawRate,
target = targetAngle,
cur = #servos > 0 and servos[1].CurrentAngle or 0,
})
end
RunService.Heartbeat:Wait()
end
log(string.format("SERVO tracking: mean err %.2f deg, max %.2f deg", servoErrSum / math.max(servoN, 1), maxServoErr))
log("t(s) speed yawRate targetAng curAng radius")
for _, s in ipairs(samples) do
local radius = math.abs(s.yaw) > 1e-4 and (s.speed / math.abs(s.yaw)) or math.huge
log(string.format("%4.1f %6.1f %7.3f %8.1f %7.1f %8.1f", s.t, s.speed, s.yaw, s.target, s.cur, radius))
end
-- Ackermann prediction for comparison.
local wheelbase = Chassis.WHEELBASE_HALF * 2
local last = samples[#samples]
if last then
local predicted = wheelbase / math.tan(math.rad(math.abs(last.cur)))
local actual = math.abs(last.yaw) > 1e-4 and (last.speed / math.abs(last.yaw)) or math.huge
log(
string.format(
"wheelbase %.1f studs | Ackermann radius at %.1f deg = %.1f studs | ACTUAL %.1f studs",
wheelbase,
math.abs(last.cur),
predicted,
actual
)
)
log(string.format("UNDERSTEER FACTOR: %.2fx (1.0 = neutral, >2 = plough)", actual / math.max(predicted, 0.01)))
end
-- Net heading change over the run.
local dot = startCF.LookVector:Dot(body.CFrame.LookVector)
log(string.format("heading change over run: %.1f deg", math.deg(math.acos(math.clamp(dot, -1, 1)))))
model:Destroy()
pad:Destroy()
return table.concat(out, "\n")
+92
View File
@@ -0,0 +1,92 @@
--[[
diag_wallmap.luau -- export every wall/barrier part near the PA as JSON so
it can be plotted top-down on the workstation.
Screen capture of the Studio viewport is unavailable on this machine
(Vinegar/Wine on a nested X display, DXVK renders the viewport so XGetImage
returns black), so instead of a screenshot we dump geometry and draw the
map ourselves. For "are there gaps / is it jumbled" questions a top-down
plot is strictly better than a screenshot anyway: it is to scale and every
part is labelled.
Read-only. Builds nothing, changes nothing.
]]
local Workspace = game:GetService("Workspace")
local WALL_NAMES = {
Barrier = true,
EdgeRail = true,
KillWall = true,
PARingWall = true,
PARingRail = true,
Median = true,
MedianFence = true,
}
local function classify(name)
if WALL_NAMES[name] then
return name
end
-- Median_12 / Road_37 style suffixed names
local base = name:match("^([%a]+)_%d+$")
if base and WALL_NAMES[base] then
return base
end
if name:find("^Road") or name:find("^PARing$") or name == "PARing" then
return "Road"
end
if name == "PAOnRamp" or name == "PAOffRamp" or name == "PARampFlare" then
return "Ramp"
end
if name == "PAApron" or name:find("Apron") then
return "Apron"
end
return nil
end
local out = {}
local counts = {}
-- The bridge truncates payloads at ~100k chars, so only export parts near the
-- PA -- that is the region under investigation anyway. Counts are still taken
-- over the WHOLE map so nothing is hidden by the filter.
local PA = require(game:GetService("ReplicatedStorage"):WaitForChild("Shared"):WaitForChild("Circuit")).PA_CENTER
local NEAR = 900
for _, d in ipairs(Workspace:GetDescendants()) do
if d:IsA("BasePart") then
local kind = classify(d.Name)
if kind then
counts[kind] = (counts[kind] or 0) + 1
local p = d.Position
local dist = math.sqrt((p.X - PA.X) ^ 2 + (p.Z - PA.Z) ^ 2)
if dist <= NEAR then
table.insert(out, {
k = kind,
x = math.floor(p.X * 10) / 10,
y = math.floor(p.Y * 10) / 10,
z = math.floor(p.Z * 10) / 10,
sx = math.floor(d.Size.X * 10) / 10,
sz = math.floor(d.Size.Z * 10) / 10,
r = math.floor(math.deg(select(2, d.CFrame:ToEulerAnglesYXZ())) * 10) / 10,
})
end
end
end
end
local summary = {}
for k, v in pairs(counts) do
table.insert(summary, string.format("%s=%d", k, v))
end
table.sort(summary)
-- execute_luau returns the RETURN VALUE, not stdout -- prints are discarded by
-- the bridge, so hand the payload back directly.
local HttpService = game:GetService("HttpService")
return HttpService:JSONEncode({
counts = table.concat(summary, " "),
total = #out,
parts = out,
})
+139
View File
@@ -0,0 +1,139 @@
--[[
diag_whatami.luau -- answer three questions the user's report raises, with
no assumptions:
Q1. Is the module in THIS DataModel the source I edited on disk?
(compare length + marker strings; disk file is 41650 bytes and
contains SHELL_YAW_OVERRIDE and bestVol)
Q2. Is the require() cache still pinned to pre-push code?
(_buildForDiag present in source but nil after require == pinned,
meaning NO fix can be in effect yet this session)
Q3. Are the cars the user is looking at STALE STAGED PROPS?
workspace.ShellReview was populated by tools/shot_shells.luau in an
earlier run. Those models are a frozen snapshot -- they are inert
Instances, not live rebuilds, so they will look identical forever
regardless of what CarManager says. Report whether ShellReview
exists, what is in it, and how each car is oriented RIGHT NOW so we
can tell "stale props" from "fix genuinely not working".
For Q3 the facing is read from the staged model directly: compare the mean
Z of the F* wheel groups against the R* groups in the model's own frame.
Front should sit at NEGATIVE Z (chassis convention: front axle built at
Z = -WHEELBASE/2). If front is at +Z the car is backwards.
]]
local ServerScriptService = 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
-- ------------------------------------------------------------------ Q1 + Q2
log("=== Q1: is the live module my edited source? ===")
local mods = {}
for _, d in ipairs(ServerScriptService:GetDescendants()) do
if d:IsA("ModuleScript") and d.Name == "CarManager" then
mods[#mods + 1] = d
end
end
log(string.format(" instances: %d", #mods))
for _, m in ipairs(mods) do
local src = ""
pcall(function()
src = m.Source
end)
log(
string.format(
" %s len=%d (disk 41650) SHELL_YAW_OVERRIDE=%s bestVol=%s",
m:GetFullName(),
#src,
tostring(src:find("SHELL_YAW_OVERRIDE", 1, true) ~= nil),
tostring(src:find("bestVol", 1, true) ~= nil)
)
)
end
log("")
log("=== Q2: is require() still pinned to old code? ===")
if #mods == 1 then
local ok, mod = pcall(require, mods[1])
if ok and typeof(mod) == "table" then
local fresh = typeof(mod._buildForDiag) == "function"
log(" _buildForDiag after require: " .. typeof(mod._buildForDiag))
log(
fresh and " => NEW code is live; fixes CAN take effect"
or " => STILL PINNED to pre-push code; NO fix can be in effect yet"
)
end
end
-- ---------------------------------------------------------------------- Q3
log("")
log("=== Q3: what is actually sitting in workspace.ShellReview? ===")
local review = workspace:FindFirstChild("ShellReview")
if not review then
log(" ShellReview does NOT exist -- user must be looking at something else")
log(" (spawned cars in-game, or the templates in ReplicatedStorage)")
else
log(" ShellReview EXISTS with " .. #review:GetChildren() .. " children.")
log(" These are FROZEN PROPS from an earlier shot_shells.luau run.")
log(" They do not rebuild themselves -- code fixes cannot change them.")
log("")
log(string.format(" %-14s %10s %10s %s", "model", "frontZ", "rearZ", "verdict"))
for _, m in ipairs(review:GetChildren()) do
if m:IsA("Model") then
local shell = m:FindFirstChild("Shell")
local wg = shell and shell:FindFirstChild("Wheels")
if not wg then
local f = m:FindFirstChild("Wheels", true)
wg = f
end
if wg then
local pivot = m:GetPivot()
local inv = pivot:Inverse()
local fS, fN, rS, rN = 0, 0, 0, 0
for _, w in ipairs(wg: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 z = (inv * (sum / n)).Z
local head = w.Name:sub(1, 1):upper()
if head == "F" then
fS, fN = fS + z, fN + 1
elseif head == "R" then
rS, rN = rS + z, rN + 1
end
end
end
if fN > 0 and rN > 0 then
local fz, rz = fS / fN, rS / rN
log(
string.format(
" %-14s %10.3f %10.3f %s",
m.Name,
fz,
rz,
fz < rz and "front at -Z OK" or "front at +Z BACKWARDS"
)
)
end
end
end
end
end
return table.concat(out, "\n")
+167
View File
@@ -0,0 +1,167 @@
--[[
diag_wheelroot.luau -- two suspected bugs in the shell wheel re-hang
(CarManager ~line 585-641).
(1) ROOT CHOICE. Line 589 picks
root = w:IsA("BasePart") and w or w:FindFirstChildWhichIsA("BasePart", true)
For a wheel group that is a Model, FindFirstChildWhichIsA returns an
ARBITRARY first BasePart -- a rotor, caliper or hub, not necessarily
the tyre. Line 621 then does delta = phys.Position - root.Position and
shifts the whole group, making THAT part concentric with the axle. If
root is not the tyre centre, the visible tyre is displaced by the
root-to-tyre offset. User reports rx7_fc wheels "sticking out from the
side of the car", which is this signature.
(2) SURVIVING OUTSIDE WELDS. Line 605 only scans shell:GetDescendants()
for welds to cut. Any weld parented OUTSIDE the shell (on the model,
on body, etc.) that ties a wheel part to the bodywork survives, which
would rigidly glue the wheel to the chassis and pin the knuckle --
the measured steering lock.
Reports, per wheel group: which part was chosen as root, how far that root
sits from the group's true centre, and any surviving weld from the group
to a part outside it.
]]
local ServerScriptService = game:GetService("ServerScriptService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
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
local Shared = ReplicatedStorage:FindFirstChild("Shared")
local Cars = require(Shared.Cars)
local Chassis = require(Shared.Chassis)
local orig = ServerScriptService:FindFirstChild("Server", true):FindFirstChild("CarManager")
local cm = orig:Clone()
cm.Parent = orig.Parent
local CarManager = require(cm)
-- ---------------------------------------------------------------- template
log("=== TEMPLATE wheel groups: what does line 589 pick as root? ===")
log(string.format("%-12s %-5s %-16s %8s %8s", "car", "grp", "root picked", "off|X|", "nParts"))
for _, t in ipairs(ReplicatedStorage.CarModels:GetChildren()) do
local wg = t:FindFirstChild("Wheels")
if wg then
for _, w in ipairs(wg:GetChildren()) do
local root = w:IsA("BasePart") and w or w:FindFirstChildWhichIsA("BasePart", true)
-- true centre of the group = AABB centre of all its parts
local lo = Vector3.new(math.huge, math.huge, math.huge)
local hi = -lo
local n = 0
if w:IsA("BasePart") then
lo, hi, n = w.Position, w.Position, 1
end
for _, d in ipairs(w:GetDescendants()) do
if d:IsA("BasePart") then
lo = Vector3.new(
math.min(lo.X, d.Position.X),
math.min(lo.Y, d.Position.Y),
math.min(lo.Z, d.Position.Z)
)
hi = Vector3.new(
math.max(hi.X, d.Position.X),
math.max(hi.Y, d.Position.Y),
math.max(hi.Z, d.Position.Z)
)
n += 1
end
end
if root and n > 0 then
local centre = (lo + hi) / 2
local off = root.Position - centre
log(string.format("%-12s %-5s %-16s %8.3f %8d", t.Name, w.Name, root.Name, math.abs(off.X), n))
end
end
end
end
-- ---------------------------------------------------------------- built
log("")
log("=== BUILT cars: tyre alignment + surviving outside welds ===")
log("tyreErrX = how far the widest shell wheel part sits from its physics")
log("cylinder along the car's lateral axis. Should be ~0 on every corner.")
log("")
log(string.format("%-12s %-5s %9s %s", "car", "grp", "tyreErrX", "welds tying group to OUTSIDE"))
for _, t in ipairs(ReplicatedStorage.CarModels:GetChildren()) do
local car = Cars.Get(t.Name)
if car then
local m = CarManager._buildForDiag(car, Chassis.tuningFor(car))
m.Parent = workspace
m:PivotTo(CFrame.new(0, 5010, 0))
for _ = 1, 4 do
RunService.Heartbeat:Wait()
end
local body = m.PrimaryPart
local shell = m:FindFirstChild("Shell")
local wg = shell and shell:FindFirstChild("Wheels")
if wg then
local inv = body.CFrame:Inverse()
for _, w in ipairs(wg:GetChildren()) do
local phys = m:FindFirstChild("Wheel_" .. w.Name:upper())
local group, inGroup = {}, {}
if w:IsA("BasePart") then
group[1], inGroup[w] = w, true
end
for _, d in ipairs(w:GetDescendants()) do
if d:IsA("BasePart") then
group[#group + 1] = d
inGroup[d] = true
end
end
if phys and #group > 0 then
-- widest part = the tyre; compare its |X| to the cylinder's
local physX = (inv * phys.Position).X
local best, bestW = nil, -1
for _, p in ipairs(group) do
local vol = p.Size.X * p.Size.Y * p.Size.Z
if vol > bestW then
best, bestW = p, vol
end
end
local errX = (inv * best.Position).X - physX
-- any weld from this group to something outside it,
-- searched across the WHOLE model, not just the shell
local bad = {}
for _, c in ipairs(m:GetDescendants()) do
if c:IsA("WeldConstraint") or c:IsA("Weld") then
local a, b = c.Part0, c.Part1
local aIn = a ~= nil and inGroup[a] or false
local bIn = b ~= nil and inGroup[b] or false
if (aIn or bIn) and not (aIn and bIn) then
local other = aIn and b or a
local oname = other and other.Name or "nil"
-- weld to its own physics cylinder is CORRECT
if oname ~= ("Wheel_" .. w.Name:upper()) then
bad[#bad + 1] = oname .. "(" .. c.Parent.Name .. ")"
end
end
end
end
log(
string.format(
"%-12s %-5s %9.3f %s",
t.Name,
w.Name,
errX,
#bad > 0 and table.concat(bad, ", ") or "-none-"
)
)
end
end
end
m:Destroy()
end
end
cm:Destroy()
return table.concat(out, "\n")
+184
View File
@@ -0,0 +1,184 @@
--[[
diag_wheelroot2.luau -- same investigation as diag_wheelroot, but wrapped
so a failure reports WHERE. The first attempt died with a bare "attempt to
call a nil value" and no line, which tells us nothing; both _buildForDiag
(CarManager:988) and Chassis.tuningFor (Chassis:158) are confirmed to
exist, so the nil is something else in the script.
Runs each stage under pcall + traceback and prints progress markers, so
the last marker printed localises the fault even if the traceback is
swallowed by the executor.
]]
local ServerScriptService = game:GetService("ServerScriptService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
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
local function stage(name, fn)
local ok, err = xpcall(fn, function(e)
return tostring(e) .. "\n" .. debug.traceback("", 2)
end)
if not ok then
log("!! FAILED at stage: " .. name)
log(err)
return false
end
log(" ok: " .. name)
return true
end
local Shared, Cars, Chassis, CarManager, cm
stage("require shared", function()
Shared = ReplicatedStorage:FindFirstChild("Shared")
Cars = require(Shared.Cars)
Chassis = require(Shared.Chassis)
end)
stage("clone+require CarManager", function()
local orig = ServerScriptService:FindFirstChild("Server", true):FindFirstChild("CarManager")
cm = orig:Clone()
cm.Parent = orig.Parent
CarManager = require(cm)
assert(typeof(CarManager._buildForDiag) == "function", "_buildForDiag is " .. typeof(CarManager._buildForDiag))
assert(typeof(Chassis.tuningFor) == "function", "tuningFor is " .. typeof(Chassis.tuningFor))
assert(typeof(Cars.Get) == "function", "Cars.Get is " .. typeof(Cars.Get))
end)
-- ------------------------------------------------- stage 1: template roots
stage("template root pick", function()
log("")
log("=== TEMPLATE: what does line 589 pick as root? ===")
log(string.format("%-12s %-5s %-18s %8s %7s", "car", "grp", "root picked", "offX", "nParts"))
for _, t in ipairs(ReplicatedStorage.CarModels:GetChildren()) do
local wg = t:FindFirstChild("Wheels")
if wg then
for _, w in ipairs(wg:GetChildren()) do
local root = w:IsA("BasePart") and w or w:FindFirstChildWhichIsA("BasePart", true)
local parts = {}
if w:IsA("BasePart") then
parts[1] = w
end
for _, d in ipairs(w:GetDescendants()) do
if d:IsA("BasePart") then
parts[#parts + 1] = d
end
end
if root and #parts > 0 then
local lo, hi = parts[1].Position, parts[1].Position
for _, p in ipairs(parts) do
lo = Vector3.new(
math.min(lo.X, p.Position.X),
math.min(lo.Y, p.Position.Y),
math.min(lo.Z, p.Position.Z)
)
hi = Vector3.new(
math.max(hi.X, p.Position.X),
math.max(hi.Y, p.Position.Y),
math.max(hi.Z, p.Position.Z)
)
end
local centre = (lo + hi) / 2
log(
string.format(
"%-12s %-5s %-18s %8.3f %7d",
t.Name,
w.Name,
root.Name,
math.abs(root.Position.X - centre.X),
#parts
)
)
end
end
end
end
end)
-- ------------------------------------------------- stage 2: built cars
stage("built car check", function()
log("")
log("=== BUILT: tyre offset from its physics cylinder + stray welds ===")
log(string.format("%-12s %-5s %9s %s", "car", "grp", "tyreErrX", "welds to OUTSIDE the group"))
for _, t in ipairs(ReplicatedStorage.CarModels:GetChildren()) do
local car = Cars.Get(t.Name)
if car then
local m = CarManager._buildForDiag(car, Chassis.tuningFor(car))
m.Parent = workspace
m:PivotTo(CFrame.new(0, 5010, 0))
for _ = 1, 4 do
RunService.Heartbeat:Wait()
end
local body = m.PrimaryPart
local shell = m:FindFirstChild("Shell")
local wg = shell and shell:FindFirstChild("Wheels")
if wg and body then
local inv = body.CFrame:Inverse()
for _, w in ipairs(wg:GetChildren()) do
local physName = "Wheel_" .. w.Name:upper()
local phys = m:FindFirstChild(physName)
local group, inGroup = {}, {}
if w:IsA("BasePart") then
group[1], inGroup[w] = w, true
end
for _, d in ipairs(w:GetDescendants()) do
if d:IsA("BasePart") then
group[#group + 1] = d
inGroup[d] = true
end
end
if phys and #group > 0 then
local physX = (inv * phys.Position).X
local best, bestV = group[1], -1
for _, p in ipairs(group) do
local v = p.Size.X * p.Size.Y * p.Size.Z
if v > bestV then
best, bestV = p, v
end
end
local errX = (inv * best.Position).X - physX
local bad = {}
for _, c in ipairs(m:GetDescendants()) do
if c:IsA("WeldConstraint") or c:IsA("Weld") then
local a, b = c.Part0, c.Part1
local aIn = (a ~= nil) and inGroup[a] or false
local bIn = (b ~= nil) and inGroup[b] or false
if (aIn or bIn) and not (aIn and bIn) then
local other = aIn and b or a
local oname = other and other.Name or "nil"
if oname ~= physName then
bad[#bad + 1] = oname
end
end
end
end
log(
string.format(
"%-12s %-5s %9.3f %s",
t.Name,
w.Name,
errX,
#bad > 0 and table.concat(bad, ", ") or "-none-"
)
)
end
end
end
m:Destroy()
end
end
end)
if cm then
cm:Destroy()
end
return table.concat(out, "\n")
+92
View File
@@ -0,0 +1,92 @@
--[[
fix_dupes.luau -- remove leftover CarManager clones and report the truth.
diag_stale found TWO ModuleScripts at ServerScriptService.Server.CarManager,
both len=30631 with no _buildForDiag, while the file on disk is 39591 bytes
and defines it. My diagnostic scripts cloned the module and did not always
destroy the clone, so duplicates accumulated; require() may bind to either.
Deletes every CarManager except the one that is a genuine child of the
Server folder (keeping the first by index), then reports what remains so
the next push has a single unambiguous target.
]]
local ServerScriptService = 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
local server = ServerScriptService:FindFirstChild("Server", true)
if not server then
return "no Server folder"
end
local mods = {}
for _, d in ipairs(ServerScriptService:GetDescendants()) do
if d:IsA("ModuleScript") and d.Name == "CarManager" then
mods[#mods + 1] = d
end
end
log(string.format("found %d CarManager module(s)", #mods))
for i, m in ipairs(mods) do
local len = "?"
pcall(function()
len = tostring(#m.Source)
end)
log(
string.format(
" [%d] %s len=%s parent=%s",
i,
m:GetFullName(),
len,
m.Parent and m.Parent:GetFullName() or "nil"
)
)
end
-- Keep exactly one: prefer a direct child of the Server folder.
local keep = nil
for _, m in ipairs(mods) do
if m.Parent == server then
keep = m
break
end
end
keep = keep or mods[1]
local removed = 0
for _, m in ipairs(mods) do
if m ~= keep then
m:Destroy()
removed += 1
end
end
log("")
log(string.format("kept: %s", keep and keep:GetFullName() or "nil"))
log(string.format("removed: %d duplicate(s)", removed))
-- Confirm the survivor
local after = 0
for _, d in ipairs(ServerScriptService:GetDescendants()) do
if d:IsA("ModuleScript") and d.Name == "CarManager" then
after += 1
end
end
log(string.format("remaining CarManager count: %d", after))
if keep then
local len = "?"
pcall(function()
len = tostring(#keep.Source)
end)
log("survivor source length: " .. len .. " (disk file is 39591)")
end
return table.concat(out, "\n")
+161
View File
@@ -0,0 +1,161 @@
#!/usr/bin/env python3
"""Render the Daikoku PA junction from Circuit.luau geometry.
MEASURE FIRST: code reasoning about elevation misleads (see project notes).
This parses the ACTUAL baked samples + the lift profile out of Circuit.luau
and draws what the builder will produce, so it can be compared against the
real aerial photographs before pushing to Studio.
Outputs a 3-panel figure:
1. top-down -- mainline vs PA ring footprint (shows the overlap)
2. elevation -- deck height through the junction (shows the clearance)
3. section -- cross-section at the PA, mainline flying over the apron
"""
import math
import re
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import Circle, Rectangle
SRC = "/home/jesse/roblox/tokyo-wangan-racing/src/shared/Circuit.luau"
OUT = "/home/jesse/roblox/tokyo-wangan-racing/tools/daikoku_check.png"
src = open(SRC).read()
PA_C = tuple(
float(x)
for x in re.search(
r"PA_CENTER = Vector3\.new\(([-\d.]+),\s*([-\d.]+),\s*([-\d.]+)\)", src
).groups()
)
PA_R = float(re.search(r"PA_RADIUS\s*=\s*([\d.]+)", src).group(1))
LIFT = float(re.search(r"PA_LIFT\s*=\s*(\d+)", src).group(1))
FLAT = int(re.search(r"LIFT_FLAT\s*=\s*(\d+)", src).group(1))
RAMPN = int(re.search(r"LIFT_RAMP\s*=\s*(\d+)", src).group(1))
raw = re.findall(
r'\{\s*(-?[\d.]+),\s*(-?[\d.]+),\s*(-?[\d.]+),\s*(\d),\s*(\d),\s*"([^"]+)"\s*\}', src
)
S = [
{"x": float(a), "y": float(b), "z": float(c), "tun": int(d) == 1}
for a, b, c, d, e, f in raw
]
n = len(S)
best, bd = 0, 1e18
for i, s in enumerate(S):
d = math.hypot(s["x"] - PA_C[0], s["z"] - PA_C[2])
if not s["tun"] and d < bd:
best, bd = i, d
def circd(i, j):
d = abs(i - j)
return min(d, n - d)
def lift_t(i):
d = circd(i, best)
if d <= FLAT:
return 1.0
if d >= FLAT + RAMPN:
return 0.0
u = (d - FLAT) / RAMPN
return 1 - (u * u * (3 - 2 * u))
for i, s in enumerate(S):
s["ly"] = s["y"] + LIFT * lift_t(i)
RING_R = PA_R + 130
RING_W = 44
ringY = PA_C[1]
entry, exitI = (best - 14) % n, (best + 14) % n
fig = plt.figure(figsize=(17, 6.2))
fig.suptitle(
"Daikoku PA junction — rebuilt from reference photos (expressway flies OVER the apron)",
fontsize=13,
fontweight="bold",
)
# ---- 1. TOP-DOWN -----------------------------------------------------------
ax = fig.add_subplot(1, 3, 1)
ax.set_title("Top-down: footprints overlap by design", fontsize=10)
win = [i for i in range(n) if circd(i, best) <= 55]
win.sort(key=lambda i: circd(i, best))
seq = sorted(win, key=lambda i: (i - best) % n if (i - best) % n < n / 2 else (i - best) % n - n)
ax.plot([S[i]["x"] for i in seq], [S[i]["z"] for i in seq], "-", color="#333", lw=7,
solid_capstyle="round", label="mainline deck (elevated)")
ax.add_patch(Rectangle((PA_C[0] - PA_R, PA_C[2] - PA_R), PA_R * 2, PA_R * 2,
fc="#9a9a9a", ec="#555", lw=1, alpha=.55, label="parking apron (ground)"))
ax.add_patch(Circle((PA_C[0], PA_C[2]), RING_R + RING_W / 2, fill=False, ec="#c44", lw=2))
ax.add_patch(Circle((PA_C[0], PA_C[2]), RING_R - RING_W / 2, fill=False, ec="#c44", lw=2,
label="PA ring road (ground)"))
for nm, idx, col in (("entry", entry, "#1a7"), ("exit", exitI, "#17a")):
r = S[idx]
tox, toz = PA_C[0] - r["x"], PA_C[2] - r["z"]
m = math.hypot(tox, toz)
rp = (PA_C[0] - tox / m * RING_R, PA_C[2] - toz / m * RING_R)
ax.plot([r["x"], rp[0]], [r["z"], rp[1]], color=col, lw=5, solid_capstyle="round",
label=f"{nm} ramp (graded)")
ax.set_aspect("equal")
ax.legend(fontsize=7, loc="upper right")
ax.set_xlabel("X (studs)")
ax.set_ylabel("Z (studs)")
ax.grid(alpha=.25)
# ---- 2. ELEVATION PROFILE --------------------------------------------------
ax2 = fig.add_subplot(1, 3, 2)
ax2.set_title("Elevation through junction: 38-stud clearance", fontsize=10)
rng = [(best + k) % n for k in range(-60, 61)]
xs = list(range(-60, 61))
ax2.plot(xs, [S[i]["y"] for i in rng], "--", color="#b00", lw=1.6,
label="BEFORE — deck at apron level (wall in road)")
ax2.plot(xs, [S[i]["ly"] for i in rng], "-", color="#0a0", lw=2.4, label="AFTER — lifted deck")
ax2.axhline(ringY, color="#777", lw=1.4, ls=":", label="PA ring / apron level")
ax2.fill_between(xs, [S[i]["ly"] for i in rng], ringY, color="#0a0", alpha=.10)
for idx, lb, col in ((entry - best, "entry", "#1a7"), (exitI - best, "exit", "#17a")):
ax2.axvline(idx, color=col, lw=1.3, ls="-.")
ax2.text(idx, ringY - 7, lb, ha="center", fontsize=7, color=col)
ax2.annotate("", xy=(0, ringY), xytext=(0, ringY + LIFT),
arrowprops=dict(arrowstyle="<->", color="#333", lw=1.4))
ax2.text(2.5, ringY + LIFT / 2, f"{LIFT:.0f} studs\nclearance", fontsize=8, va="center")
ax2.set_xlabel("sample offset from PA centre")
ax2.set_ylabel("deck height Y (studs)")
ax2.legend(fontsize=7, loc="lower left")
ax2.grid(alpha=.25)
# ---- 3. CROSS-SECTION ------------------------------------------------------
ax3 = fig.add_subplot(1, 3, 3)
ax3.set_title("Section at PA — matches reference photos", fontsize=10)
ax3.add_patch(Rectangle((-PA_R, ringY - 3), PA_R * 2, 3, fc="#9a9a9a", ec="#555"))
ax3.text(0, ringY - 9, "parking apron (ground level)", ha="center", fontsize=8)
for sgn in (-1, 1):
ax3.add_patch(Rectangle((sgn * RING_R - RING_W / 2, ringY - 3), RING_W, 4,
fc="#c44", ec="#822"))
ax3.text(RING_R, ringY + 6, "ring road", ha="center", fontsize=7, color="#822")
deckY = ringY + LIFT
ax3.add_patch(Rectangle((-bd - 60, deckY), 120, 5, fc="#333", ec="#000"))
ax3.text(-bd, deckY + 12, "elevated mainline", ha="center", fontsize=8, fontweight="bold")
for px in (-bd - 40, -bd + 40):
ax3.add_patch(Rectangle((px - 4, ringY - 2), 8, LIFT + 2, fc="#bbb", ec="#777"))
for cx in (-160, -60, 40, 140, 240):
ax3.add_patch(Rectangle((cx, ringY, ), 26, 9, fc="#4a7fb5", ec="#245"))
ax3.annotate("", xy=(-bd + 70, ringY), xytext=(-bd + 70, deckY),
arrowprops=dict(arrowstyle="<->", color="#0a0", lw=1.6))
ax3.text(-bd + 78, ringY + LIFT / 2, f"{LIFT:.0f} studs\n(cars pass under)",
fontsize=8, va="center", color="#070")
ax3.set_xlim(-PA_R - 220, PA_R + 220)
ax3.set_ylim(ringY - 22, deckY + 30)
ax3.set_xlabel("distance from PA centre (studs)")
ax3.set_ylabel("Y (studs)")
ax3.grid(alpha=.25)
fig.tight_layout(rect=[0, 0, 1, .94])
fig.savefig(OUT, dpi=118)
print("wrote", OUT)
print(f"nearest sample={best} dist={bd:.1f} ring outer={RING_R + RING_W / 2:.1f}"
f" overlap={RING_R + RING_W / 2 - bd:+.1f} studs")
+163
View File
@@ -0,0 +1,163 @@
#!/usr/bin/env python3
"""
plot_wallmap.py -- render the JSON dumped by tools/diag_wallmap.luau as a
top-down PNG.
WHY THIS EXISTS: the Studio viewport cannot be screenshotted on this machine
(Vinegar/Wine on nested X display :1, DXVK-rendered -> XGetImage returns
black). For "is there a gap / does it look jumbled / which way does it face"
questions, plotting the datamodel geometry top-down is both possible and more
precise than a screenshot.
Usage:
python3 tools/plot_wallmap.py /tmp/wallmap_raw.txt /tmp/wallmap.png ["Title"]
The input file is whatever rbx.call('execute_luau', ...) returned for
diag_wallmap.luau -- it may be JSON or a Python-repr dict, both are handled.
"""
import ast
import json
import math
import subprocess
import sys
STYLE = {
"Road": ("#3a3a44", 0.5),
"Ramp": ("#4a4a58", 0.5),
"Apron": ("#2e2e36", 0.5),
"Barrier": ("#ffffff", 0.95),
"EdgeRail": ("#8a8a9a", 0.95),
"KillWall": ("#ff4444", 0.95),
"PARingWall": ("#00e5ff", 0.95),
"PARingRail": ("#0090a0", 0.95),
"Median": ("#ffd400", 0.95),
"MedianFence": ("#c0a000", 0.95),
}
# Draw decks first so walls land on top of them.
ORDER = [
"Road", "Ramp", "Apron", "Median", "MedianFence",
"KillWall", "EdgeRail", "Barrier", "PARingWall", "PARingRail",
]
LEGEND = [
("PARingWall (ring parapet)", "#00e5ff"),
("Barrier (mainline)", "#ffffff"),
("KillWall", "#ff4444"),
("Median", "#ffd400"),
("Road/Ramp deck", "#3a3a44"),
]
def load(path):
raw = open(path).read()
start = raw.find("{")
if start < 0:
raise SystemExit(f"no payload found in {path}")
body = raw[start:]
try:
return json.loads(body)
except json.JSONDecodeError:
# The bridge often hands back a Python repr (single quotes, True/None).
return ast.literal_eval(body)
def analyse(parts):
"""Print the numbers that answer 'is it sealed' before drawing anything."""
ring = [p for p in parts if p["k"] == "PARingWall"]
if not ring:
return None, None
cx = sum(p["x"] for p in ring) / len(ring)
cz = sum(p["z"] for p in ring) / len(ring)
rad = [math.hypot(p["x"] - cx, p["z"] - cz) for p in ring]
split = (min(rad) + max(rad)) / 2
inner = [p for p in ring if math.hypot(p["x"] - cx, p["z"] - cz) < split]
outer = [p for p in ring if math.hypot(p["x"] - cx, p["z"] - cz) >= split]
print(f"PA centre ~({cx:.0f},{cz:.0f}) radii {min(rad):.0f}..{max(rad):.0f}")
print(f"inner-edge walls {len(inner)}/72 outer-edge walls {len(outer)}/72")
angs = sorted(math.degrees(math.atan2(p["z"] - cz, p["x"] - cx)) % 360 for p in inner)
gaps = []
for i, a in enumerate(angs):
nxt = angs[(i + 1) % len(angs)]
d = (nxt - a) % 360
if d > 7.5: # one segment is 5 deg; >7.5 means at least one is absent
gaps.append((round(a, 1), round(nxt, 1), round(d, 1)))
print("INNER-wall gaps >7.5deg (a usable exit shows here):", gaps or "NONE - sealed")
road = sum(1 for p in parts if p["k"] == "Road")
bar = sum(1 for p in parts if p["k"] == "Barrier")
if road:
# One barrier per side of every deck segment => healthy ratio ~2.0.
print(f"mainline Road={road} Barrier={bar} ratio={bar/road:.2f} (healthy ~2.0)")
return cx, cz
def render(parts, cx, cz, out_png, title, span=1000.0, size=1400):
def sx(x):
return (x - cx) / span * (size / 2) + size / 2
def sy(z):
return (z - cz) / span * (size / 2) + size / 2
svg = [
f'<svg xmlns="http://www.w3.org/2000/svg" width="{size}" height="{size}">'
f'<rect width="{size}" height="{size}" fill="#0e0e12"/>'
]
for kind in ORDER:
col, op = STYLE.get(kind, ("#888888", 0.9))
for p in parts:
if p["k"] != kind:
continue
w = max(p["sx"] / span * (size / 2), 1.5)
h = max(p["sz"] / span * (size / 2), 1.5)
# Negate yaw: world +Z runs down the screen in this projection.
svg.append(
f'<g transform="translate({sx(p["x"]):.1f},{sy(p["z"]):.1f}) rotate({-p["r"]:.1f})">'
f'<rect x="{-w/2:.1f}" y="{-h/2:.1f}" width="{w:.1f}" height="{h:.1f}"'
f' fill="{col}" opacity="{op}"/></g>'
)
svg.append(f'<circle cx="{sx(cx):.0f}" cy="{sy(cz):.0f}" r="5" fill="#39ff14"/>')
svg.append(
f'<text x="20" y="34" fill="#eee" font-family="monospace" font-size="21">{title}</text>'
)
for i, (lab, col) in enumerate(LEGEND):
yy = 64 + i * 25
svg.append(
f'<rect x="20" y="{yy-11}" width="16" height="13" fill="{col}"/>'
f'<text x="44" y="{yy}" fill="#bbb" font-family="monospace" font-size="16">{lab}</text>'
)
svg.append("</svg>")
svg_path = out_png.rsplit(".", 1)[0] + ".svg"
open(svg_path, "w").write("\n".join(svg))
# rsvg-convert gives cleaner output but is often not installed; fall back to
# ImageMagick. FileNotFoundError (binary absent) must be caught too -- a
# returncode check alone does not cover it.
try:
rc = subprocess.run(
["rsvg-convert", "-w", str(size), svg_path, "-o", out_png],
capture_output=True,
).returncode
except FileNotFoundError:
rc = 1
if rc != 0:
subprocess.run(["convert", "-density", "96", svg_path, out_png], check=True)
print("wrote", out_png)
def main():
src = sys.argv[1] if len(sys.argv) > 1 else "/tmp/wallmap_raw.txt"
dst = sys.argv[2] if len(sys.argv) > 2 else "/tmp/wallmap.png"
title = sys.argv[3] if len(sys.argv) > 3 else "Daikoku PA -- wall map"
data = load(src)
parts = data["parts"]
print("counts (whole map):", data.get("counts"))
print("plotted parts:", len(parts))
cx, cz = analyse(parts)
if cx is None:
raise SystemExit("no PARingWall parts found -- nothing to centre on")
render(parts, cx, cz, dst, title)
if __name__ == "__main__":
main()
+93
View File
@@ -0,0 +1,93 @@
--[[
proof_marker.luau -- prove, visually, that the bridge is driving the SAME
Studio the user is looking at.
Every claim I have made so far has been a number I read back myself, which
is exactly the kind of evidence that failed us: I verified a push at 41650
bytes three times and the cars still never changed. So this puts something
in the user's viewport that they can confirm with their own eyes.
Creates a large, bright, obviously-artificial marker high above the spawn
area, named with a fixed token so it is unmistakable in Explorer, plus a
floating billboard label. If the user sees it, the bridge and their Studio
are the same place, and the remaining problem is purely about WHERE source
is written (Edit vs Play datamodel). If they do NOT see it, the bridge is
attached to a different place/instance and nothing else matters until that
is fixed.
Deliberately not subtle. Cleaned up later by name.
]]
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
local TOKEN = "HERMES_PROOF_MARKER"
-- remove any previous marker so we never confuse an old one for a new one
for _, d in ipairs(workspace:GetDescendants()) do
if d.Name == TOKEN then
d:Destroy()
end
end
-- Anchor near whatever the player/camera is likely looking at: use the first
-- player's character if present, else the spawn, else world origin.
local anchor = Vector3.new(0, 0, 0)
local Players = game:GetService("Players")
local plr = Players:GetPlayers()[1]
if plr and plr.Character and plr.Character.PrimaryPart then
anchor = plr.Character.PrimaryPart.Position
log("anchored to player character: " .. plr.Name)
else
local sp = workspace:FindFirstChildWhichIsA("SpawnLocation", true)
if sp then
anchor = sp.Position
log("anchored to SpawnLocation")
else
log("anchored to world origin")
end
end
local part = Instance.new("Part")
part.Name = TOKEN
part.Size = Vector3.new(40, 40, 40)
part.Position = anchor + Vector3.new(0, 60, 0)
part.Anchored = true
part.CanCollide = false
part.Material = Enum.Material.Neon
part.Color = Color3.fromRGB(255, 0, 255)
part.Transparency = 0.15
part.Parent = workspace
local gui = Instance.new("BillboardGui")
gui.Name = TOKEN .. "_GUI"
gui.Size = UDim2.new(0, 600, 0, 160)
gui.StudsOffset = Vector3.new(0, 30, 0)
gui.AlwaysOnTop = true
gui.Parent = part
local label = Instance.new("TextLabel")
label.Size = UDim2.new(1, 0, 1, 0)
label.BackgroundTransparency = 0.25
label.BackgroundColor3 = Color3.fromRGB(0, 0, 0)
label.TextColor3 = Color3.fromRGB(255, 255, 0)
label.TextScaled = true
label.Font = Enum.Font.SourceSansBold
label.Text = "HERMES IS HERE\nif you can read this we are\nin the SAME Studio place"
label.Parent = gui
log("")
log("created " .. TOKEN .. " at " .. tostring(part.Position))
log("size 40x40x40, magenta neon, billboard label, AlwaysOnTop")
log("")
log("USER CHECK: look up near your car / spawn, or search Explorer for")
log(" " .. TOKEN)
log("If it is visible -> bridge and your Studio are the same place.")
return table.concat(out, "\n")
+121
View File
@@ -0,0 +1,121 @@
#!/usr/bin/env python3
"""Push MapBuilder.luau source into a LIVE (Play-mode) Studio via the MCP bridge.
Rojo cannot reach the running server datamodel in Play mode, so the source is
shipped as ~9000-char chunks into named StringValue children (p0, p1, ...) under
ReplicatedStorage, then concatenated and assigned to the ModuleScript's .Source.
Named children matter: a retried chunk overwrites its slot instead of appending
a duplicate. Bridge calls fail intermittently ("Unable to find an active Studio
instance") even while Studio is up, so every call is retried.
"""
import sys, time
sys.path.insert(0, "/tmp")
import rbx
SRC_PATH = sys.argv[1]
MODULE_NAME = sys.argv[2]
# Which datamodel to write into. "Server" is the PLAY-mode sandbox: Studio
# DISCARDS it when Play stops, so pushes there are silently lost on the next
# stop/start (this burned an entire debugging session -- pushes verified at the
# correct byte length, then vanished). "Edit" is the persistent datamodel and
# is what you almost always want; it survives into the next Play session.
# Edit is only reachable while Studio is actually in Edit mode.
DATAMODEL = sys.argv[3] if len(sys.argv) > 3 else "Edit"
MODULE = 'game:GetService("ServerScriptService"):FindFirstChild("Server", true)'
CHUNK = 9000
def call(code, tries=4):
last = None
for a in range(tries):
try:
r = rbx.call(
"execute_luau",
{"code": code, "datamodel_type": DATAMODEL},
warmup=6,
wait=45,
timeout=300,
)
if r and "Unable to find an active Studio" not in str(r):
return r
last = r
except Exception as e: # noqa: BLE001
last = e
time.sleep(5)
raise RuntimeError(f"bridge failed after {tries} tries: {last}")
src = open(SRC_PATH, encoding="utf-8").read()
nbytes = len(src.encode())
print(f"source: {len(src)} chars, {nbytes} utf-8 bytes")
# Pick a long-bracket level that does not occur in the source.
lvl = 0
while ("]" + "=" * lvl + "]") in src or ("[" + "=" * lvl + "[") in src:
lvl += 1
eq = "=" * lvl
print(f"long-bracket level: {lvl}")
chunks = [src[i : i + CHUNK] for i in range(0, len(src), CHUNK)]
print(f"chunks: {len(chunks)}")
print(call('local rs=game:GetService("ReplicatedStorage")\n'
'local b=rs:FindFirstChild("MBBuf") if b then b:Destroy() end\n'
'b=Instance.new("Folder") b.Name="MBBuf" b.Parent=rs\n'
'return "buffer reset"'))
for i, c in enumerate(chunks):
# Lua long-bracket literals SWALLOW a newline that immediately follows the
# opening bracket. When a chunk happened to start with "\n" that byte was
# silently lost, giving assembled=65158 vs expected=65159. Emit a deliberate
# leading newline for Lua to eat so the payload always survives intact.
code = (
'local rs=game:GetService("ReplicatedStorage")\n'
'local b=rs:WaitForChild("MBBuf")\n'
f'local n="p{i}"\n'
"local sv=b:FindFirstChild(n) or Instance.new(\"StringValue\")\n"
"sv.Name=n sv.Parent=b\n"
f"sv.Value=[{eq}[\n{c}]{eq}]\n"
f'return "p{i} "..#sv.Value'
)
print(f" chunk {i+1}/{len(chunks)}: {call(code)}")
assemble = f"""
local rs=game:GetService("ReplicatedStorage")
local b=rs:WaitForChild("MBBuf")
local parts={{}}
for i=0,{len(chunks)-1} do
local sv=b:FindFirstChild("p"..i)
if not sv then return "MISSING CHUNK p"..i end
parts[#parts+1]=sv.Value
end
local s=table.concat(parts)
-- Search the WHOLE game, not just ServerScriptService: shared modules live in
-- ReplicatedStorage and client ones in StarterPlayerScripts. Searching only
-- SSS silently reported "NOT FOUND" for Chassis while the wrapper still
-- exited 0, so a half-applied push looked like a success.
local roots={{
game:GetService("ServerScriptService"),
game:GetService("ReplicatedStorage"),
game:GetService("StarterPlayer"),
game:GetService("Workspace"),
}}
local m=nil
for _,r in ipairs(roots) do
local hit=r:FindFirstChild("{MODULE_NAME}", true)
if hit and hit:IsA("ModuleScript") then m=hit break end
end
if not m then return "{MODULE_NAME} ModuleScript NOT FOUND" end
m.Source=s
return ("assembled=%d expected={nbytes} match=%s class=%s path=%s"):format(
#s, tostring(#s=={nbytes}), m.ClassName, m:GetFullName())
"""
result = call(assemble)
print(result)
# Exit non-zero on any failure. Previously this always exited 0, so a
# "NOT FOUND" or byte-count mismatch was reported as success by the caller.
if "match=true" not in str(result):
print("PUSH FAILED -- module not updated", file=sys.stderr)
sys.exit(1)
+83
View File
@@ -0,0 +1,83 @@
-- Rebuild the map with a BUSTED require() cache.
--
-- PITFALL (cost a build cycle on 2026-08-02): require() caches per Studio
-- session and keyed by INSTANCE. Pushing new .Source into the existing
-- ModuleScript does NOT invalidate that cache, so `require(MapBuilder)`
-- happily returns the OLD compiled module and the map rebuilds from stale
-- code -- the diagnostic then reports failures that the source has already
-- fixed (legacy PARingPillar present, no ramp segments).
--
-- Fix: swap in CLONES. A clone is a different instance, so it misses the
-- cache and is compiled fresh. Circuit must be swapped too, because
-- MapBuilder resolves it BY PATH -- putting the clone at the same path is
-- what makes the fresh MapBuilder pick up the fresh Circuit.
local SSS = game:GetService("ServerScriptService")
local RS = game:GetService("ReplicatedStorage")
local mbOrig = SSS.Server.MapBuilder
local ciOrig = RS.Shared.Circuit
-- Clone both, park the originals under a holder so the paths are free.
local holder = game:GetService("ServerStorage"):FindFirstChild("_cachebust")
if holder then
holder:Destroy()
end
holder = Instance.new("Folder")
holder.Name = "_cachebust"
holder.Parent = game:GetService("ServerStorage")
local ciClone = ciOrig:Clone()
local mbClone = mbOrig:Clone()
ciOrig.Parent = holder
ciOrig.Name = "Circuit_old"
mbOrig.Parent = holder
mbOrig.Name = "MapBuilder_old"
ciClone.Name = "Circuit"
ciClone.Parent = RS.Shared
mbClone.Name = "MapBuilder"
mbClone.Parent = SSS.Server
local out = {}
local function say(s)
table.insert(out, s)
end
say("swapped in fresh clones (require cache bypassed)")
local old = workspace:FindFirstChild("Map") or workspace:FindFirstChild("Circuit")
if old then
old:Destroy()
say("destroyed previous Map")
end
local ok, err = pcall(function()
local MB = require(mbClone)
MB.build()
end)
if not ok then
say("BUILD_ERROR: " .. tostring(err))
return table.concat(out, "\n")
end
local m = workspace:FindFirstChild("Map") or workspace:FindFirstChild("Circuit")
local n = 0
if m then
n = #m:GetDescendants()
end
say("BUILD_OK parts=" .. tostring(n))
-- Quick confirmation that the NEW code path actually ran.
local counts = { PARingPillar = 0, PAEntryRamp = 0, PAExitRamp = 0, PARampPillar = 0, PARampKerb = 0 }
for _, d in ipairs(workspace:GetDescendants()) do
for k in pairs(counts) do
if string.sub(d.Name, 1, #k) == k then
counts[k] += 1
end
end
end
for k, v in pairs(counts) do
say((" %-14s %d"):format(k, v))
end
return table.concat(out, "\n")
+108
View File
@@ -0,0 +1,108 @@
--[[
shot_shells.luau -- render the built cars for visual confirmation.
Numeric cues have now been wrong twice in this investigation (wheel-name
probes silently matched the chassis; the "asym" proxy gave verdicts that
contradicted the boot cue). The boot check is self-consistent but cannot
judge hatch_junk or hyper_x -- which are precisely the two cars in
question. So look at them.
Places all five cars in a row, all facing the SAME chassis direction
(forward = -Z), and points a camera down the row from the side. On a
correct fleet every nose points the same way; a backwards shell is
obvious. Leaves the cars in Workspace under a folder so a screenshot can
be taken from Studio.
]]
local ServerScriptService = game:GetService("ServerScriptService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local Shared = ReplicatedStorage:FindFirstChild("Shared")
local Cars = require(Shared.Cars)
local Chassis = require(Shared.Chassis)
local orig = ServerScriptService:FindFirstChild("Server", true):FindFirstChild("CarManager")
local cm = orig:Clone()
cm.Parent = orig.Parent
local CarManager = require(cm)
local old = workspace:FindFirstChild("ShellReview")
if old then
old:Destroy()
end
local folder = Instance.new("Folder")
folder.Name = "ShellReview"
folder.Parent = workspace
local pad = Instance.new("Part")
pad.Size = Vector3.new(200, 4, 120)
pad.Position = Vector3.new(0, 4998, 0)
pad.Anchored = true
pad.Color = Color3.fromRGB(70, 70, 75)
pad.Material = Enum.Material.SmoothPlastic
pad.Parent = folder
local out = {}
local i = 0
local ids = {}
for _, t in ipairs(ReplicatedStorage.CarModels:GetChildren()) do
table.insert(ids, t.Name)
end
table.sort(ids)
for _, id in ipairs(ids) do
local car = Cars.Get(id)
if car then
local m = CarManager._buildForDiag(car, Chassis.tuningFor(car))
m.Name = "car_" .. id
m.Parent = folder
-- All cars share one orientation: forward = -Z, i.e. identity rotation.
m:PivotTo(CFrame.new(-60 + i * 30, 5003, 0))
for _, p in ipairs(m:GetDescendants()) do
if p:IsA("BasePart") then
p.Anchored = true
end
end
-- Label above each car so the screenshot is self-describing.
local sign = Instance.new("Part")
sign.Size = Vector3.new(0.2, 0.2, 0.2)
sign.Transparency = 1
sign.Anchored = true
sign.CanCollide = false
sign.Position = Vector3.new(-60 + i * 30, 5010, 0)
sign.Parent = folder
local gui = Instance.new("BillboardGui")
gui.Size = UDim2.new(0, 260, 0, 60)
gui.AlwaysOnTop = true
gui.Parent = sign
local lbl = Instance.new("TextLabel")
lbl.Size = UDim2.fromScale(1, 1)
lbl.BackgroundTransparency = 0.3
lbl.BackgroundColor3 = Color3.new(0, 0, 0)
lbl.TextColor3 = Color3.new(1, 1, 1)
lbl.TextScaled = true
lbl.Font = Enum.Font.GothamBold
lbl.Text = id
lbl.Parent = gui
table.insert(out, id)
i += 1
end
end
for _ = 1, 10 do
RunService.Heartbeat:Wait()
end
-- Side-on camera: looking along +X at the row, so every nose should point
-- to the SAME side of frame (screen left = -Z).
local cam = workspace.CurrentCamera
if cam then
cam.CameraType = Enum.CameraType.Scriptable
cam.CFrame = CFrame.lookAt(Vector3.new(0, 5022, 78), Vector3.new(0, 5004, 0))
end
cm:Destroy()
return "placed: "
.. table.concat(out, ", ")
.. "\nAll cars share forward = -Z (screen LEFT in this view)."
.. "\nCamera set side-on. Every nose should point the same way."
+161
View File
@@ -0,0 +1,161 @@
--[[
verify_convention.luau -- prove the forward convention is now consistent.
Deliberately NOT measuring anything derived from a constant. The previous
harness read Wheel_* positions, which come straight from
Chassis.WHEELBASE_HALF (5.9) -- identical for every car whether or not any
fix applied, so it passed vacuously. Here every assertion is a RELATIONSHIP
that genuinely differs between the broken and fixed builds:
1. The steering servo must sit on the axle that LEADS under throttle.
Broken build: servos on the trailing axle (shopping-cart caster).
2. The shell nose must point the same way the seat drives.
Broken build: 180 degrees opposed.
3. DRIVE_SIGN must spin the wheels toward the direction of travel.
Builds each car via the real path (Chassis.tuningFor + _buildForDiag) in
the Edit datamodel, where require() is fresh.
]]
local SSS = game:GetService("ServerScriptService")
local RS = game:GetService("ReplicatedStorage")
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
local function findModule(name: string): ModuleScript?
for _, root in ipairs({ SSS, RS }) do
local hit = root:FindFirstChild(name, true)
if hit and hit:IsA("ModuleScript") then
return hit
end
end
return nil
end
local cmMod = findModule("CarManager")
local carsMod = findModule("Cars")
local chassisMod = findModule("Chassis")
if not (cmMod and carsMod and chassisMod) then
return "FATAL: missing module(s)"
end
local CarManager = require(cmMod)
local Cars = require(carsMod)
local Chassis = require(chassisMod)
if typeof(CarManager._buildForDiag) ~= "function" then
return "FATAL: _buildForDiag absent -- stale CarManager, refusing to report"
end
log(string.format("Chassis.DRIVE_SIGN = %s", tostring(Chassis.DRIVE_SIGN)))
log("")
local carList = Cars.List or Cars.All
if typeof(carList) ~= "table" or #carList == 0 then
return "FATAL: empty car list"
end
log(string.format("%-11s %-22s %-24s %s", "car", "steered axle", "shell nose vs drive", "verdict"))
log(string.rep("-", 78))
local tested, failed = 0, 0
for _, car in ipairs(carList) do
if typeof(car) ~= "table" or not car.Id then
continue
end
local tuning = Chassis.tuningFor(car)
local ok, model = pcall(CarManager._buildForDiag, car, tuning)
if not ok or typeof(model) ~= "Instance" then
log(string.format("%-11s BUILD FAILED: %s", car.Id, tostring(model)))
failed += 1
continue
end
local body = model:FindFirstChild("Body")
local seat
for _, d in ipairs(model:GetDescendants()) do
if d:IsA("VehicleSeat") then
seat = d
break
end
end
if not (body and body:IsA("BasePart") and seat) then
log(string.format("%-11s NO BODY/SEAT", car.Id))
failed += 1
model:Destroy()
continue
end
tested += 1
local inv = body.CFrame:Inverse()
-- Direction of travel under positive throttle, in body-local space.
local driveZ = inv:VectorToObjectSpace(seat.CFrame.LookVector).Z
local drivePos = driveZ > 0
-- (1) Which axle carries the steering servos?
local servoZ, servoN = 0, 0
for _, d in ipairs(model:GetDescendants()) do
if d:IsA("HingeConstraint") and d.ActuatorType == Enum.ActuatorType.Servo then
local host = d.Parent
if host and host:IsA("BasePart") then
servoZ += (inv * host.CFrame).Z
servoN += 1
end
end
end
local steerLeads, steerTxt = false, "no servos"
if servoN > 0 then
local sm = servoZ / servoN
steerLeads = (drivePos and sm > 0) or (not drivePos and sm < 0)
steerTxt = string.format("z=%+.2f %s", sm, steerLeads and "LEADS" or "TRAILS")
end
-- (2) Shell nose vs direction of travel.
local shell
for _, c in ipairs(model:GetChildren()) do
if c:IsA("Model") then
shell = c
break
end
end
local shellOk, shellTxt = true, "no shell"
if shell then
local shellLook = shell:GetPivot().LookVector
local seatLook = seat.CFrame.LookVector
local a = Vector3.new(shellLook.X, 0, shellLook.Z)
local b = Vector3.new(seatLook.X, 0, seatLook.Z)
if a.Magnitude > 1e-4 and b.Magnitude > 1e-4 then
local deg = math.deg(math.acos(math.clamp(a.Unit:Dot(b.Unit), -1, 1)))
shellOk = deg < 30
shellTxt = string.format("%.1f deg %s", deg, shellOk and "OK" or "FLIPPED")
end
end
local pass = (servoN == 0 or steerLeads) and shellOk
if not pass then
failed += 1
end
log(string.format("%-11s %-22s %-24s %s", car.Id, steerTxt, shellTxt, pass and "PASS" or "FAIL"))
model:Destroy()
end
log("")
if tested == 0 then
log(">>> NOTHING TESTED -- this is a FAILURE, not a pass.")
elseif failed > 0 then
log(string.format(">>> %d/%d FAILED", failed, tested))
else
log(string.format(">>> ALL %d CARS PASS: steered axle leads, shell faces travel", tested))
end
return table.concat(out, "\n")
+185
View File
@@ -0,0 +1,185 @@
--[[
verify_fixes.luau -- build all five cars with the NEWLY PUSHED code and
measure whether the two fixes actually work. Runs in the EDIT datamodel,
where require() is fresh (no Play-session cache pin), so this is the first
honest test of these fixes.
Checks, per car:
FIX 1 -- FACING (hatch_junk, hyper_x rendered backwards)
Compare mean Z of F* wheel groups vs R* groups in the model's own frame.
Chassis convention: front axle is built at NEGATIVE Z. So frontZ < rearZ
means the car faces the right way. hatch_junk / hyper_x previously came
out reversed because their shell wheel LABELS are swapped, which fooled
the auto-solver into a 180 degree rotation; SHELL_YAW_OVERRIDE pins them
to yaw 0.
FIX 2 -- WHEEL STANCE (rx7_fc wheels protruding from the bodywork)
For each wheel, measure |X| of the wheel's visual centre against |X| of
the axle it hangs from. The old code picked an arbitrary BasePart as the
wheel root (often a rotor/caliper ~0.96 studs inboard of the tyre) and
made THAT concentric with the axle, shoving the tyre outboard by the
difference. Now it picks the largest-volume part (the tyre). Residual
offset should be near zero; anything > 0.35 studs is a visible protrusion.
Everything is measured from the built model, not inferred from source.
Models are destroyed afterwards so Edit is left clean.
]]
local ServerScriptService = 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
local serverFolder = ServerScriptService:FindFirstChild("Server", true)
local cmMod = serverFolder and serverFolder:FindFirstChild("CarManager")
if not cmMod then
return "FATAL: no CarManager module found"
end
log(
string.format(
"CarManager source: %d bytes, SHELL_YAW_OVERRIDE=%s",
#cmMod.Source,
tostring(cmMod.Source:find("SHELL_YAW_OVERRIDE", 1, true) ~= nil)
)
)
local okCM, CarManager = pcall(require, cmMod)
if not okCM then
return "FATAL: require(CarManager) failed: " .. tostring(CarManager)
end
if typeof(CarManager._buildForDiag) ~= "function" then
return "FATAL: _buildForDiag is "
.. typeof(CarManager._buildForDiag)
.. " -- require() served STALE code, results would be meaningless"
end
log("_buildForDiag present -> NEW code is genuinely running")
-- find Cars + Tuning modules
local function findModule(name)
for _, svc in ipairs({ game:GetService("ReplicatedStorage"), ServerScriptService }) do
local m = svc:FindFirstChild(name, true)
if m and m:IsA("ModuleScript") then
return m
end
end
return nil
end
local carsMod = findModule("Cars")
local chassisMod = findModule("Chassis")
if not carsMod then
return "FATAL: Cars module not found"
end
if not chassisMod then
return "FATAL: Chassis module not found (needed for tuningFor)"
end
local Cars = require(carsMod)
local Chassis = require(chassisMod)
if typeof(Chassis.tuningFor) ~= "function" then
return "FATAL: Chassis.tuningFor is " .. typeof(Chassis.tuningFor)
end
local FRONT_NEG_Z = true -- chassis builds front axle at -Z
local PROTRUDE_TOL = 0.35
log("")
log(string.format("%-10s %8s %8s %-12s | %8s %-10s", "car", "frontZ", "rearZ", "FACING", "maxOff", "STANCE"))
log(string.rep("-", 74))
local built = {}
local anyFail = false
local carList = Cars.List or Cars.All
if typeof(carList) ~= "table" or #carList == 0 then
return "FATAL: could not find a non-empty car list (Cars.List). " .. "Refusing to report a vacuous pass."
end
log(string.format("car list: %d entries", #carList))
for _, car in ipairs(carList) do
if typeof(car) == "table" and car.Id then
local tuning = Chassis.tuningFor(car)
local ok, model = pcall(CarManager._buildForDiag, car, tuning)
if not ok or typeof(model) ~= "Instance" then
log(string.format("%-10s BUILD FAILED: %s", car.Id, tostring(model)))
anyFail = true
else
model.Parent = workspace
built[#built + 1] = model
local pivotInv = model:GetPivot():Inverse()
-- ---- FACING: mean Z of front vs rear wheel groups
local fS, fN, rS, rN = 0, 0, 0, 0
-- ---- STANCE: wheel centre |X| vs its axle |X|
local maxOff = 0
for _, d in ipairs(model:GetDescendants()) do
if d:IsA("BasePart") and d.Name:match("^Wheel_") then
local lbl = d.Name:sub(7, 7):upper()
local z = (pivotInv * d.CFrame).Z
if lbl == "F" then
fS, fN = fS + z, fN + 1
elseif lbl == "R" then
rS, rN = rS + z, rN + 1
end
-- visual tyre centre = centroid of the attached shell parts
local axleX = (pivotInv * d.CFrame).X
local sum, n = Vector3.zero, 0
for _, c in ipairs(d:GetDescendants()) do
if c:IsA("BasePart") then
sum, n = sum + c.Position, n + 1
end
end
if n > 0 then
local visX = (pivotInv * CFrame.new(sum / n)).X
local off = math.abs(math.abs(visX) - math.abs(axleX))
if off > maxOff then
maxOff = off
end
end
end
end
local facing, stance = "n/a", "n/a"
if fN > 0 and rN > 0 then
local fz, rz = fS / fN, rS / rN
local good = FRONT_NEG_Z and (fz < rz) or (fz > rz)
facing = good and "OK" or "BACKWARDS"
if not good then
anyFail = true
end
stance = (maxOff <= PROTRUDE_TOL) and "OK" or "PROTRUDING"
if maxOff > PROTRUDE_TOL then
anyFail = true
end
log(string.format("%-10s %8.2f %8.2f %-12s | %8.3f %-10s", car.Id, fz, rz, facing, maxOff, stance))
else
log(string.format("%-10s (no Wheel_* parts found)", car.Id))
anyFail = true
end
end
end
end
for _, m in ipairs(built) do
m:Destroy()
end
local tested = #built
log("")
if tested == 0 then
log(">>> NOTHING WAS TESTED -- no cars built. This is a FAILURE, not a pass.")
elseif anyFail then
log(string.format(">>> SOME CHECKS FAILED (%d cars tested)", tested))
else
log(string.format(">>> ALL CHECKS PASSED (%d cars tested)", tested))
end
return table.concat(out, "\n")
+90
View File
@@ -0,0 +1,90 @@
--[[
verify_push.luau -- verify a push by INSPECTING THE DATAMODEL, not by
trusting push_module.py's self-report.
push_module.py reported "assembled=39591 expected=39591 match=true" while
the live module was in fact 30631 bytes with no _buildForDiag. That check
only proves the script reassembled its own chunks correctly client-side;
it does not prove the DataModel accepted the write. This closes that gap.
Reports, for every CarManager in ServerScriptService:
* instance count (must be 1 -- diagnostic clones caused duplicates)
* Source length vs the expected 39591
* presence of marker strings that only exist in the new source
* the module's exported key set after a fresh require
]]
local ServerScriptService = game:GetService("ServerScriptService")
local EXPECTED_LEN = 41650
local MARKERS = {
"SHELL_YAW_OVERRIDE",
"bestVol",
"_buildForDiag",
}
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
local mods = {}
for _, d in ipairs(ServerScriptService:GetDescendants()) do
if d:IsA("ModuleScript") and d.Name == "CarManager" then
mods[#mods + 1] = d
end
end
log(string.format("CarManager instances: %d %s", #mods, #mods == 1 and "OK" or "*** should be exactly 1 ***"))
for i, m in ipairs(mods) do
local src = nil
pcall(function()
src = m.Source
end)
if not src then
log(string.format(" [%d] %s <source unreadable>", i, m:GetFullName()))
else
log(string.format(" [%d] %s", i, m:GetFullName()))
log(
string.format(
" length %d vs expected %d %s",
#src,
EXPECTED_LEN,
#src == EXPECTED_LEN and "MATCH" or "*** STALE / MISMATCH ***"
)
)
for _, mk in ipairs(MARKERS) do
log(string.format(" marker %-26s %s", mk, src:find(mk, 1, true) and "present" or "*** MISSING ***"))
end
end
end
if #mods == 1 then
log("")
local ok, mod = pcall(require, mods[1])
if ok and typeof(mod) == "table" then
local keys = {}
for k in pairs(mod) do
keys[#keys + 1] = tostring(k)
end
table.sort(keys)
log("require() keys: " .. table.concat(keys, ", "))
log(
"_buildForDiag: "
.. typeof(mod._buildForDiag)
.. (
typeof(mod._buildForDiag) == "function" and " <- new source IS live"
or " <- STILL RUNNING OLD CODE (require cache from Play start)"
)
)
else
log("require failed: " .. tostring(mod))
end
end
return table.concat(out, "\n")
+63
View File
@@ -0,0 +1,63 @@
--[[
verify_revert.luau -- confirm the reverted convention is in Edit source.
Reads .Source text (never require(), which caches for the Studio session).
Asserts the NEW-BAD tokens are ABSENT and the ORIGINAL-GOOD tokens PRESENT,
so a stale copy fails loudly instead of passing.
]]
local RS = game:GetService("ReplicatedStorage")
local SSS = game:GetService("ServerScriptService")
local function find(name)
for _, root in ipairs({ SSS, RS, game:GetService("ServerStorage"), workspace }) do
local m = root:FindFirstChild(name, true)
if m and m:IsA("ModuleScript") then
return m
end
end
return nil
end
local out = {}
local pass, fail = 0, 0
local function check(label, cond, detail)
if cond then
pass += 1
table.insert(out, string.format(" %-34s OK %s", label, detail or ""))
else
fail += 1
table.insert(out, string.format(" %-34s FAIL %s", label, detail or ""))
end
end
local cm = find("CarManager")
local ch = find("Chassis")
if not cm or not ch then
return "FATAL: CarManager or Chassis not found in Edit"
end
local cmSrc = cm.Source
local chSrc = ch.Source
table.insert(out, string.format("CarManager %d bytes %s", #cmSrc, cm:GetFullName()))
check("isFront reverted to zSign < 0", cmSrc:find("isFront = zSign < 0", 1, true) ~= nil, "")
check("bad zSign > 0 GONE", cmSrc:find("isFront = zSign > 0", 1, true) == nil, "")
check("shell solver back on -Z", cmSrc:find("atan2(nose.X, -nose.Z)", 1, true) ~= nil, "")
check("bad solver GONE", cmSrc:find("atan2(-nose.X, nose.Z)", 1, true) == nil, "")
check("regression warning recorded", cmSrc:find("made things strictly worse", 1, true) ~= nil, "")
table.insert(out, string.format("Chassis %d bytes %s", #chSrc, ch:GetFullName()))
check("DRIVE_SIGN reverted to -1", chSrc:find("DRIVE_SIGN = -1", 1, true) ~= nil, "")
check("bad DRIVE_SIGN = 1 GONE", chSrc:find("DRIVE_SIGN = 1 ", 1, true) == nil, "")
table.insert(out, "")
table.insert(out, string.format(">>> %d passed, %d failed", pass, fail))
if fail > 0 then
table.insert(out, ">>> REVERT INCOMPLETE -- do not press Play")
else
table.insert(out, ">>> Revert confirmed in Edit source. Save (Ctrl+S) before Play.")
table.insert(out, " NOTE: this proves the SOURCE is reverted. It does NOT prove the")
table.insert(out, " cars render correctly -- only your eyes in Play can show that.")
end
return table.concat(out, "\n")
+138
View File
@@ -0,0 +1,138 @@
--[[
verify_rwd.luau -- verify the RWD steering fix, bypassing the require cache.
check_live_source proved the pushed source IS in the DataModel (40307
bytes, RWD marker present, single copy). But diag_steering still saw
4 motors because require() caches per module INSTANCE for the life of the
Play session, and CarManager was first required before the push -- so the
old build closure was still running.
Clone() produces a NEW instance, so require(clone) re-executes the current
source. That gives a true read of the fix without restarting Play mode.
]]
local ServerScriptService = game:GetService("ServerScriptService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
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
local Shared = ReplicatedStorage:FindFirstChild("Shared")
local Cars = require(Shared.Cars)
local Chassis = require(Shared.Chassis)
local orig = ServerScriptService:FindFirstChild("Server", true):FindFirstChild("CarManager")
local clone = orig:Clone()
clone.Parent = orig.Parent
local CarManager = require(clone)
log("required a FRESH clone of CarManager (" .. #clone.Source .. " bytes)")
local pad = Instance.new("Part")
pad.Size = Vector3.new(6000, 8, 6000)
pad.Position = Vector3.new(0, 5000, 0)
pad.Anchored = true
pad.Material = Enum.Material.Concrete
pad.Parent = workspace
local padTop = pad.Position.Y + 4
local car = Cars.Get("gtr_r34")
local tuning = Chassis.tuningFor(car)
local model = CarManager._buildForDiag(car, tuning)
model.Parent = workspace
model:PivotTo(CFrame.new(0, padTop + 3, 0))
local servos, motors = {}, {}
for _, d in ipairs(model:GetDescendants()) do
if d:IsA("HingeConstraint") then
if d.Name == Chassis.STEER_SERVO_NAME then
table.insert(servos, d)
elseif d.Name == Chassis.WHEEL_MOTOR_NAME then
table.insert(motors, d)
end
end
end
local motorCorners = {}
for _, mo in ipairs(motors) do
table.insert(motorCorners, mo.Parent and mo.Parent.Name or "?")
end
log(string.format("servos %d | motors %d [%s]", #servos, #motors, table.concat(motorCorners, ", ")))
log(
string.format(
"expected: 2 servos, 2 motors on REAR corners only -> %s",
(#motors == 2 and #servos == 2) and "OK" or "WRONG"
)
)
log("")
local body = model.PrimaryPart
for _ = 1, 90 do
RunService.Heartbeat:Wait()
end
-- Drive the rears and hold a steady steer angle, exactly like diag_steering.
local TARGET = -14
local wheelR = Chassis.WHEEL_RADIUS
local topW = tuning.MaxWheelAngularVelocity
log("t(s) speed yawRate targetAng curAng radius")
local samples = 0
local errSum, errMax = 0, 0
local startLook = body.CFrame.LookVector
for step = 1, 300 do
for _, mo in ipairs(motors) do
mo.MotorMaxTorque = tuning.MotorMaxTorque
mo.AngularVelocity = -topW
end
for _, s in ipairs(servos) do
s.TargetAngle = TARGET
end
if step % 30 == 0 then
local speed = body.AssemblyLinearVelocity.Magnitude
local yaw = body.AssemblyAngularVelocity.Y
local cur = servos[1].CurrentAngle
local radius = (math.abs(yaw) > 1e-4) and (speed / math.abs(yaw)) or math.huge
log(string.format("%4.1f %6.1f %7.3f %8.1f %8.1f %9.1f", step / 60, speed, yaw, TARGET, cur, radius))
samples += 1
local e = math.abs(cur - TARGET)
errSum += e
errMax = math.max(errMax, e)
end
RunService.Heartbeat:Wait()
end
local finalLook = body.CFrame.LookVector
local heading = math.deg(math.acos(math.clamp(startLook:Dot(finalLook), -1, 1)))
log("")
log(string.format("SERVO tracking: mean err %.2f deg, max %.2f deg", errSum / samples, errMax))
log(string.format("heading change over run: %.1f deg", heading))
local speed = body.AssemblyLinearVelocity.Magnitude
local yaw = math.abs(body.AssemblyAngularVelocity.Y)
local actualR = (yaw > 1e-4) and (speed / yaw) or math.huge
local wheelbase = Chassis.WHEELBASE or 11.8
local ackR = wheelbase / math.tan(math.rad(math.abs(servos[1].CurrentAngle)))
log(
string.format(
"Ackermann radius at %.1f deg = %.1f | ACTUAL %.1f | understeer %.2fx",
servos[1].CurrentAngle,
ackR,
actualR,
actualR / ackR
)
)
model:Destroy()
pad:Destroy()
clone:Destroy()
return table.concat(out, "\n")
+92
View File
@@ -0,0 +1,92 @@
--[[
verify_source.luau -- read the module SOURCE TEXT in Edit, bypassing require().
Why: require() caches a ModuleScript's return value for the whole Studio
session. Both modules were pushed successfully (match=true), but an earlier
require() in this same session means require() still hands back the OLD
table -- so a harness that builds cars via require() measures stale code and
can report a false PASS. Reading .Source is cache-proof.
This only proves the SOURCE is correct on disk-in-Studio. It cannot prove
runtime behaviour -- that needs a fresh datamodel (press Play), which
rebuilds the require cache from this source.
]]
local SSS = game:GetService("ServerScriptService")
local RS = game:GetService("ReplicatedStorage")
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
local function find(name: string): ModuleScript?
for _, root in ipairs({ SSS, RS }) do
local hit = root:FindFirstChild(name, true)
if hit and hit:IsA("ModuleScript") then
return hit
end
end
return nil
end
local checks = {
{
module = "CarManager",
label = "isFront convention",
want = "local isFront = zSign > 0",
bad = "local isFront = zSign < 0",
},
{
module = "CarManager",
label = "shell nose solver",
want = "shellYaw = -math.atan2(-nose.X, nose.Z)",
bad = "shellYaw = -math.atan2(nose.X, -nose.Z)",
},
{
module = "CarManager",
label = "SHELL_YAW_OVERRIDE removed",
want = nil,
bad = "local SHELL_YAW_OVERRIDE",
},
{
module = "Chassis",
label = "DRIVE_SIGN",
want = "Chassis.DRIVE_SIGN = 1",
bad = "Chassis.DRIVE_SIGN = -1",
},
}
local allGood = true
for _, c in ipairs(checks) do
local m = find(c.module)
if not m then
log(string.format("%-28s MODULE NOT FOUND (%s)", c.label, c.module))
allGood = false
continue
end
local src = m.Source
local hasWant = (c.want == nil) or (string.find(src, c.want, 1, true) ~= nil)
local hasBad = (c.bad ~= nil) and (string.find(src, c.bad, 1, true) ~= nil)
local ok = hasWant and not hasBad
if not ok then
allGood = false
end
log(string.format("%-28s %-6s [%s: %d bytes]", c.label, ok and "OK" or "STALE", m:GetFullName(), #src))
end
log("")
log(
allGood and ">>> SOURCE IS CORRECT in Edit. require() is still caching the old"
or ">>> SOURCE IS STALE -- the push did not land."
)
if allGood then
log(" copies this session, so only a FRESH Play session can prove runtime")
log(" behaviour. Save (Ctrl+S), then press Play.")
end
return table.concat(out, "\n")