Aux-lane suppression + PA geometry fixes; probe/verify tooling
CI / quality (push) Failing after 1m9s

MapBuilder.luau
  - Suppress the aux lane across samples 525-550 (AUX_SUPPRESS_FROM/TO),
    honoring the user's hand deletion in Studio so a rebuild no longer
    regenerates it.
  - Smoothstep the width ramp over AUX_BLEND=5 samples on each side. A hard
    cut produced a 26-stud lateral fence jog; the blend removes it.
  - DO NOT remove this suppression. Re-enabling it resurrects geometry the
    user deleted by hand.

rokit.toml
  - rojo 7.5.1 -> 7.7.0 to match the auto-updating Studio plugin.
    Server older than plugin fails the handshake with
    "attempt to index number with 'protocolVersion'".

tools/
  - 34 read-only probe/verify/capture scripts used to measure the datamodel
    instead of reasoning from source.

Verified-good baseline at this commit:
  total parts       16788
  PAWestSlip        60 segs, 34.4 deg total bend, worst 1.4, 0 kinks >5
  PAEastSlip        60 segs, 30.7 deg total bend, worst 1.3, 0 kinks >5
  AuxLane parts     30 (samples 525-550 deliberately absent)
  barrier coverage  823 samples, 0 unguarded
  fence jogs >10st  0
This commit is contained in:
2026-08-06 08:27:47 -04:00
parent c297b20aed
commit 6e50f9718d
36 changed files with 3723 additions and 1 deletions
+194
View File
@@ -0,0 +1,194 @@
-- Verify the speed-change (auxiliary) lanes and that the slips no longer cut
-- across mainline traffic.
--
-- Rebuilds from a CLONED module first (require() cache bypass), then measures.
-- Every check below can FAIL -- and asserts it actually tested something, so a
-- lookup returning nothing reports FAIL rather than vacuously passing.
local sss = game:GetService("ServerScriptService")
local ss = game:GetService("ServerStorage")
local ws = game:GetService("Workspace")
local RS = game:GetService("ReplicatedStorage")
local realMod = sss:FindFirstChild("MapBuilder", true)
if not realMod then return "MapBuilder not found" end
local bust = ss:FindFirstChild("_cachebust")
if not bust then
bust = Instance.new("Folder")
bust.Name = "_cachebust"
bust.Parent = ss
end
for _, ch in ipairs(bust:GetChildren()) do
ch:Destroy()
end
local clone = realMod:Clone()
clone.Name = "MapBuilder_aux_" .. tostring(math.random(1e6, 9e6))
clone.Parent = bust
local mb = require(clone)
local old = ws:FindFirstChild("Map")
if old then old:Destroy() end
local ok, err = pcall(function() mb.build() end)
if not ok then return "BUILD ERROR: " .. tostring(err) end
local map = ws:FindFirstChild("Map")
if not map then return "no Map after build" end
local Circuit = require(RS:WaitForChild("Shared"):WaitForChild("Circuit"))
local out = {}
local function say(s) table.insert(out, s) end
-- Collect the aux lane decks and the slip decks.
local aux = {}
local decks = { PAWestSlip = {}, PAEastSlip = {} }
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
local ai = d.Name:match("^AuxLane_(%d+)$")
if ai then
table.insert(aux, { i = tonumber(ai), p = d.Position, s = d.Size, cf = d.CFrame })
end
for slipName, list in pairs(decks) do
local idx = d.Name:match("^" .. slipName .. "_Deck_(%d+)$")
if idx then
table.insert(list, { i = tonumber(idx), p = d.Position, s = d.Size, cf = d.CFrame })
end
end
end
end
-- 1. The aux lanes must exist at all.
if #aux == 0 then
say("FAIL: no AuxLane_* parts were built")
return table.concat(out, "\n")
end
say(("aux lane segments built: %d"):format(#aux))
-- 2. Every aux segment must sit OUTSIDE the mainline deck, not on top of it.
-- Measure the gap between the mainline centreline and the aux centreline,
-- and compare against that sample's own half-width.
local samples = Circuit.samples
local minClear, minAt = math.huge, -1
local tested = 0
for _, a in ipairs(aux) do
local s = samples[a.i]
if s then
tested += 1
local d = (Vector3.new(a.p.X, 0, a.p.Z) - Vector3.new(s.pos.X, 0, s.pos.Z)).Magnitude
-- inner edge of aux lane relative to mainline centre
local innerEdge = d - a.s.X / 2
if innerEdge < minClear then
minClear, minAt = innerEdge, a.i
end
end
end
if tested == 0 then
say("FAIL: no aux segment matched a circuit sample (harness looked up nothing)")
else
say(("aux inner edge vs mainline centre: min %.1f studs at sample %d (tested %d)")
:format(minClear, minAt, tested))
end
-- 3. The slip decks must START at the aux lane offset, not on the centreline.
-- This is the actual bug the user reported: lat began at 0.
for slipName, list in pairs(decks) do
table.sort(list, function(x, y) return x.i < y.i end)
if #list < 50 then
say(("%s FAIL: only %d deck segments"):format(slipName, #list))
else
local first = list[1]
-- Find the nearest mainline sample to the slip's first segment.
local bestD, bestI = math.huge, -1
for i = 1, #samples do
local d = (Vector3.new(first.p.X, 0, first.p.Z) - Vector3.new(samples[i].pos.X, 0, samples[i].pos.Z)).Magnitude
if d < bestD then bestD, bestI = d, i end
end
local halfW = 0
do
-- Deck half-width at that sample, read off the built road itself.
local road = map:FindFirstChild("Road_" .. bestI, true)
halfW = road and road.Size.X / 2 or 0
end
local innerEdge = bestD - first.s.X / 2
local verdict = (halfW > 0 and innerEdge >= halfW - 1.5) and "PASS" or "FAIL"
say(("%s start: centre %.1f from mainline centre, inner edge %.1f, deck half-width %.1f -> %s")
:format(slipName, bestD, innerEdge, halfW, verdict))
end
end
-- 4. Continuity: the slip mouth and the nearest aux segment must be adjacent
-- (end-to-end), not overlapping and not separated by a hole.
for slipName, list in pairs(decks) do
if #list >= 50 then
local first = list[1]
local bestGap, bestI = math.huge, -1
for _, a in ipairs(aux) do
local g = (a.p - first.p).Magnitude
if g < bestGap then bestGap, bestI = g, a.i end
end
local verdict = bestGap <= 90 and "PASS" or "FAIL"
say(("%s mouth -> nearest aux segment: %.1f studs (sample %d) -> %s")
:format(slipName, bestGap, bestI, verdict))
end
end
-- 5. The aux lane must be level with the mainline deck at the mouth.
local maxStep, stepAt = 0, -1
for _, a in ipairs(aux) do
local road = map:FindFirstChild("Road_" .. a.i, true)
if road then
local step = math.abs((a.p.Y + a.s.Y / 2) - (road.Position.Y + road.Size.Y / 2))
if step > maxStep then maxStep, stepAt = step, a.i end
end
end
say(("aux/mainline surface step: max %.2f studs at sample %d -> %s")
:format(maxStep, stepAt, maxStep <= 0.05 and "PASS" or "FAIL"))
-- 6. The taper must actually taper: widths should fall to near zero at the end.
local wmin, wmax = math.huge, 0
for _, a in ipairs(aux) do
wmin = math.min(wmin, a.s.X)
wmax = math.max(wmax, a.s.X)
end
say(("aux width range: %.1f .. %.1f studs -> %s")
:format(wmin, wmax, (wmax >= 25 and wmin <= 8) and "PASS (full lane + taper)" or "CHECK"))
-- 7. DECELERATION LANES. Each mouth must now have lane on BOTH sides: one
-- upstream (decel, for traffic leaving) and one downstream (accel, for
-- traffic joining). Group the aux segments into runs and check each slip
-- mouth has a run on either side of it.
table.sort(aux, function(x, y) return x.i < y.i end)
local runs, cur = {}, { aux[1] }
for k = 2, #aux do
if aux[k].i - aux[k - 1].i > 3 then
table.insert(runs, cur); cur = {}
end
table.insert(cur, aux[k])
end
table.insert(runs, cur)
say(("aux runs: %d (expect 4 = 2 mouths x {decel, accel})"):format(#runs))
for ri, r in ipairs(runs) do
local wlo, whi = math.huge, 0
for _, a in ipairs(r) do
wlo = math.min(wlo, a.s.X); whi = math.max(whi, a.s.X)
end
say((" run %d: samples %d..%d (%d segs) width %.1f..%.1f")
:format(ri, r[1].i, r[#r].i, #r, wlo, whi))
end
-- Each slip join must sit between a decel run and an accel run.
for _, slip in ipairs({ { "PAWestSlip", 525 }, { "PAEastSlip", 551 } }) do
local name, join = slip[1], slip[2]
local before, after = false, false
for _, r in ipairs(runs) do
if r[#r].i < join and join - r[#r].i <= 6 then before = true end
if r[1].i > join and r[1].i - join <= 6 then after = true end
if r[1].i <= join and r[#r].i >= join then
-- run spans the join: counts as adjacent on both sides
before = true; after = true
end
end
say(("%s join=%d: decel-side run %s, accel-side run %s -> %s")
:format(name, join, tostring(before), tostring(after),
(before and after) and "PASS" or "FAIL"))
end
return table.concat(out, "\n")