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:
@@ -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 |
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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,
|
||||
})
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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,
|
||||
})
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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()
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
@@ -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."
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
Reference in New Issue
Block a user